#!/usr/bin/env python3
"""Bank batch-173: Rule 131 body-part lexical expansion."""

from __future__ import annotations

import datetime as dt
import json
import re
import sqlite3
import sys

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-173"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-173-body-part-lexical-expansion-process.md"

BODY_HEADS = {
    "mara": "hand",
    "jina": "foot",
    "walu": "face",
    "milka": "ear",
    "miyil": "eye",
    "ngandal": "mouth",
    "dukul": "head",
    "muku": "back",
    "bibar": "shin",
}
DENY = {
    "yah",
    "ngawuya",
    "walnga",
    "maja-nga",
    "yunga",
    "bada-bada",
    "wangkar-wangkar",
    "biyul",
    "warrur",
}
FORBIDDEN_MARKERS = ("-ngarr", "-ngurr", "-kuda", "-l-urr", "wawu-")
PRONOUN_A = {"ngayu", "ngana", "nganjin", "yundu", "yurra", "jana", "nyulu"}
TRANSITIVE_PAST = ("nyajin", "julurrin", "yijarrin", "baykan", "bijan", "daray-manin")


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


ROWS = [
    (1, "Grandmother's hand is good.",
     "Babi mara ngulkurr.",
     "babi mara ngulkurr | grandmother hand good",
     "r131_state_grandmother_hand_good", 2, js(["babi", "mara", "ngulkurr"]), js([]),
     "mara hand dictionary; Patz §3.2.2.3 notes mara is the free noun root for hand; inalienable body-part apposition after Rule 129/130 controls.", "0.86"),
    (2, "Father's foot is sore.",
     "Nganjan jina kaka.",
     "nganjan jina kaka | father foot sore",
     "r131_state_father_foot_sore", 2, js(["nganjan", "jina", "kaka"]), js([]),
     "jina foot dictionary; Patz nickname/source and foot examples; kaka pain/sore is sourced as bodily state, not a new body-part head.", "0.86"),
    (3, "Mother's face is wet.",
     "Ngamu walu duna.",
     "ngamu walu duna | mother face wet",
     "r131_state_mother_face_wet", 2, js(["ngamu", "walu", "duna"]), js([]),
     "walu face dictionary; Patz appearance/body-part examples; retained plain body-part predicate frame.", "0.86"),
    (4, "The old woman's ear is sore.",
     "Mukul-jalbu milka kaka.",
     "mukul-jalbu milka kaka | old-woman ear sore",
     "r131_state_old_woman_ear_sore", 2, js(["mukul", "jalbu", "milka", "kaka"]), js([]),
     "milka ear dictionary and Patz body-part compounds; kaka sore restricted to pain sense.", "0.86"),
    (5, "The young man's eye is sore.",
     "Warru miyil kaka.",
     "warru miyil kaka | young.man eye sore",
     "r131_state_young_man_eye_sore", 2, js(["warru", "miyil", "kaka"]), js([]),
     "miyil eye dictionary; Patz eye compounds; tree sense excluded by gloss and English.", "0.86"),
    (6, "The old man's mouth is dry.",
     "Mukul-dingkar ngandal marral.",
     "mukul-dingkar ngandal marral | old-man mouth dry",
     "r131_state_old_man_mouth_dry", 2, js(["mukul", "dingkar", "ngandal", "marral"]), js([]),
     "ngandal mouth dictionary and Patz phonetic table; retained dry predicate frame.", "0.86"),
    (7, "Grandfather's head is big.",
     "Kami dukul yalbay.",
     "kami dukul yalbay | grandfather head big",
     "r131_state_grandfather_head_big", 2, js(["kami", "dukul", "yalbay"]), js([]),
     "dukul head dictionary; Patz gives nyulu dukul yalbay 'his big head'.", "0.88"),
    (8, "The woman's back is sore.",
     "Jalbu muku kaka.",
     "jalbu muku kaka | woman back sore",
     "r131_state_woman_back_sore", 2, js(["jalbu", "muku", "kaka"]), js([]),
     "muku back dictionary; Patz examples Ngayu muku kaka; owner changed, frame retained.", "0.88"),
    (9, "The child's shin is sore.",
     "Karrkay bibar kaka.",
     "karrkay bibar kaka | child shin sore",
     "r131_state_child_shin_sore", 2, js(["karrkay", "bibar", "kaka"]), js([]),
     "bibar shin/ankle dictionary; Patz case table gives bibar-a 'shin'; shin sense selected.", "0.84"),
    (10, "The baby's ear is good.",
     "Ngawa milka ngulkurr.",
     "ngawa milka ngulkurr | baby ear good",
     "r131_state_baby_ear_good", 2, js(["ngawa", "milka", "ngulkurr"]), js([]),
     "milka ear dictionary; plain inalienable body-part state with retained ngulkurr.", "0.86"),

    (11, "Grandmother saw the child's hand.",
     "Babi-ngka karrkay mara nyajin.",
     "babi-ngka karrkay mara nyaji-ny | grandmother-ERG child hand see-PAST",
     "r131_perception_grandmother_child_hand", 4, js(["babi", "karrkay", "mara", "nyajil"]), js([]),
     "Whole-part O apposition modelled on Patz affected-body examples; common-noun perceiver ERG.", "0.86"),
    (12, "I saw the child's foot.",
     "Ngayu karrkay jina nyajin.",
     "ngayu karrkay jina nyaji-ny | I child foot see-PAST",
     "r131_perception_pronoun_child_foot", 4, js(["ngayu", "karrkay", "jina", "nyajil"]), js([]),
     "jina foot, not track/path; pronoun A remains bare in nyaji-l frame.", "0.86"),
    (13, "Mother saw the baby's face.",
     "Ngamu-ngku ngawa walu nyajin.",
     "ngamu-ngku ngawa walu nyaji-ny | mother-ERG baby face see-PAST",
     "r131_perception_mother_baby_face", 4, js(["ngamu", "ngawa", "walu", "nyajil"]), js([]),
     "walu face in whole-part O; common-noun perceiver ERG.", "0.86"),
    (14, "Father saw the old woman's ear.",
     "Nganjan-angka mukul-jalbu milka nyajin.",
     "nganjan-angka mukul-jalbu milka nyaji-ny | father-ERG old-woman ear see-PAST",
     "r131_perception_father_old_woman_ear", 4, js(["nganjan", "mukul", "jalbu", "milka", "nyajil"]), js([]),
     "milka ear in whole-part O; common-noun perceiver ERG.", "0.86"),
    (15, "Grandfather saw the girl's eye.",
     "Kami-ngka maral miyil nyajin.",
     "kami-ngka maral miyil nyaji-ny | grandfather-ERG girl eye see-PAST",
     "r131_perception_grandfather_girl_eye", 4, js(["kami", "maral", "miyil", "nyajil"]), js([]),
     "miyil eye sense selected; common-noun perceiver ERG.", "0.86"),
    (16, "The old woman saw the child's mouth.",
     "Mukul-jalbu-ngku karrkay ngandal nyajin.",
     "mukul-jalbu-ngku karrkay ngandal nyaji-ny | old-woman-ERG child mouth see-PAST",
     "r131_perception_old_woman_child_mouth", 4, js(["mukul", "jalbu", "karrkay", "ngandal", "nyajil"]), js([]),
     "ngandal mouth dictionary and Patz table; common-noun perceiver ERG.", "0.86"),
    (17, "The man saw the dog's head.",
     "Dingkar-angka kaya dukul nyajin.",
     "dingkar-angka kaya dukul nyaji-ny | man-ERG dog head see-PAST",
     "r131_perception_man_dog_head", 4, js(["dingkar", "kaya", "dukul", "nyajil"]), js([]),
     "dukul head core sense; whole-part O apposition.", "0.86"),
    (18, "The woman saw the old man's back.",
     "Jalbu-ngku mukul-dingkar muku nyajin.",
     "jalbu-ngku mukul-dingkar muku nyaji-ny | woman-ERG old-man back see-PAST",
     "r131_perception_woman_old_man_back", 4, js(["jalbu", "mukul", "dingkar", "muku", "nyajil"]), js([]),
     "muku back core sense; common-noun perceiver ERG.", "0.86"),
    (19, "The child saw father's shin.",
     "Karrkay-ngku nganjan bibar nyajin.",
     "karrkay-ngku nganjan bibar nyaji-ny | child-ERG father shin see-PAST",
     "r131_perception_child_father_shin", 4, js(["karrkay", "nganjan", "bibar", "nyajil"]), js([]),
     "bibar shin selected over ankle; common-noun perceiver ERG.", "0.84"),
    (20, "I saw grandfather's hand.",
     "Ngayu kami mara nyajin.",
     "ngayu kami mara nyaji-ny | I grandfather hand see-PAST",
     "r131_perception_pronoun_grandfather_hand", 4, js(["ngayu", "kami", "mara", "nyajil"]), js([]),
     "mara hand whole-part O; pronoun A bare.", "0.86"),

    (21, "Mother washed grandmother's hand.",
     "Ngamu-ngku babi mara julurrin.",
     "ngamu-ngku babi mara julurri-ny | mother-ERG grandmother hand wash-PAST",
     "r131_wash_mother_grandmother_hand", 4, js(["ngamu", "babi", "mara", "julurril"]), js([]),
     "Transitive wash of whole-part O extends Batch 144 face/mouth wash frame to sourced mara hand.", "0.84"),
    (22, "Grandfather washed the child's foot.",
     "Kami-ngka karrkay jina julurrin.",
     "kami-ngka karrkay jina julurri-ny | grandfather-ERG child foot wash-PAST",
     "r131_wash_grandfather_child_foot", 4, js(["kami", "karrkay", "jina", "julurril"]), js([]),
     "jina foot in whole-part O; common-noun A ERG.", "0.84"),
    (23, "The woman washed the baby's face.",
     "Jalbu-ngku ngawa walu julurrin.",
     "jalbu-ngku ngawa walu julurri-ny | woman-ERG baby face wash-PAST",
     "r131_wash_woman_baby_face", 4, js(["jalbu", "ngawa", "walu", "julurril"]), js([]),
     "walu face in retained transitive wash frame.", "0.86"),
    (24, "The old woman washed the child's mouth.",
     "Mukul-jalbu-ngku karrkay ngandal julurrin.",
     "mukul-jalbu-ngku karrkay ngandal julurri-ny | old-woman-ERG child mouth wash-PAST",
     "r131_wash_old_woman_child_mouth", 4, js(["mukul", "jalbu", "karrkay", "ngandal", "julurril"]), js([]),
     "ngandal mouth in retained transitive wash frame; no instrument imported.", "0.86"),
    (25, "Father washed the child's shin.",
     "Nganjan-angka karrkay bibar julurrin.",
     "nganjan-angka karrkay bibar julurri-ny | father-ERG child shin wash-PAST",
     "r131_wash_father_child_shin", 4, js(["nganjan", "karrkay", "bibar", "julurril"]), js([]),
     "bibar shin in whole-part O; common-noun A ERG.", "0.84"),
    (26, "I washed my hand.",
     "Ngayu mara julurri-jin.",
     "ngayu mara julurri-ji-ny | I hand wash-REF-PAST",
     "r131_reflexive_wash_my_hand", 3, js(["ngayu", "mara", "julurriji"]), js([]),
     "Patz successive example has woman hand wash-REF; pronoun S bare, no possessive suffix.", "0.86"),
    (27, "Mother washed her face.",
     "Ngamu walu julurri-jin.",
     "ngamu walu julurri-ji-ny | mother face wash-REF-PAST",
     "r131_reflexive_wash_mother_face", 3, js(["ngamu", "walu", "julurriji"]), js([]),
     "Reflexive wash frame with sourced walu face; no transitive ERG because reflexive/S frame.", "0.84"),
    (28, "The girl washed her hands.",
     "Maral mara julurri-jin.",
     "maral mara julurri-ji-ny | girl hand wash-REF-PAST",
     "r131_reflexive_wash_girl_hand", 3, js(["maral", "mara", "julurriji"]), js([]),
     "Patz hand-wash reflexive frame; maral S bare.", "0.86"),

    (29, "Mother put water on grandmother's hand.",
     "Ngamu-ngku bana yijarrin babi-nda mara-nga.",
     "ngamu-ngku bana yijarri-ny babi-nda mara-nga | mother-ERG water put-PAST grandmother-LOC:pt hand-LOC",
     "r131_place_water_grandmother_hand", 4, js(["ngamu", "bana", "yijarril", "babi", "mara"]), js([]),
     "Retained placement frame; mara-nga follows prior sourced body-part LOC pattern; person LOC:pt marked.", "0.84"),
    (30, "Father put water on the child's foot.",
     "Nganjan-angka bana yijarrin karrkay-anda jina-nga.",
     "nganjan-angka bana yijarri-ny karrkay-anda jina-nga | father-ERG water put-PAST child-LOC:pt foot-LOC",
     "r131_place_water_child_foot", 4, js(["nganjan", "bana", "yijarril", "karrkay", "jina"]), js([]),
     "jina-nga foot-LOC is directly modelled on Patz example 521.", "0.88"),
    (31, "Grandmother put water on the baby's back.",
     "Babi-ngka bana yijarrin ngawa-nda muku-ngu.",
     "babi-ngka bana yijarri-ny ngawa-nda muku-ngu | grandmother-ERG water put-PAST baby-LOC:pt back-LOC",
     "r131_place_water_baby_back", 4, js(["babi", "bana", "yijarril", "ngawa", "muku"]), js([]),
     "muku back dictionary; body-part LOC retained from prior source-backed batch pattern.", "0.84"),
    (32, "The child put the stone in father's hand.",
     "Karrkay-ngku kulji yijarrin nganjan-anda mara-nga.",
     "karrkay-ngku kulji yijarri-ny nganjan-anda mara-nga | child-ERG stone put-PAST father-LOC:pt hand-LOC",
     "r131_place_stone_father_hand", 4, js(["karrkay", "kulji", "yijarril", "nganjan", "mara"]), js([]),
     "Retained placement frame; kulji stone, not mountain; mara-nga body-part location.", "0.84"),
    (33, "The man put the stone on the woman's foot.",
     "Dingkar-angka kulji yijarrin jalbu-ndu jina-nga.",
     "dingkar-angka kulji yijarri-ny jalbu-ndu jina-nga | man-ERG stone put-PAST woman-LOC:pt foot-LOC",
     "r131_place_stone_woman_foot", 4, js(["dingkar", "kulji", "yijarril", "jalbu", "jina"]), js([]),
     "jina-nga foot-LOC; common-noun A ERG; no track/path sense.", "0.86"),
    (34, "Grandmother put water on the old man's shin.",
     "Babi-ngka bana yijarrin mukul-dingkar-anda bibar-a.",
     "babi-ngka bana yijarri-ny mukul-dingkar-anda bibar-a | grandmother-ERG water put-PAST old-man-LOC:pt shin-LOC",
     "r131_place_water_old_man_shin", 4, js(["babi", "bana", "yijarril", "mukul", "dingkar", "bibar"]), js([]),
     "bibar-a shin-LOC is directly attested in Patz case table; shin sense selected.", "0.86"),
    (35, "I put water in my mouth.",
     "Ngayu bana yijarrin ngandal-ba.",
     "ngayu bana yijarri-ny ngandal-ba | I water put-PAST mouth-LOC",
     "r131_place_pronoun_water_mouth", 4, js(["ngayu", "bana", "yijarril", "ngandal"]), js([]),
     "ngandal mouth dictionary; body-part LOC follows existing mouth-location pattern; pronoun A bare.", "0.84"),
    (36, "The woman put a flower in the child's hand.",
     "Jalbu-ngku nganka yijarrin karrkay-anda mara-nga.",
     "jalbu-ngku nganka yijarri-ny karrkay-anda mara-nga | woman-ERG flower put-PAST child-LOC:pt hand-LOC",
     "r131_place_flower_child_hand", 4, js(["jalbu", "nganka", "yijarril", "karrkay", "mara"]), js([]),
     "nganka flower from Rule 129; placement into sourced mara hand location; common-noun A ERG.", "0.84"),

    (37, "The dog bit grandfather's hand.",
     "Kaya-ngka kami mara baykan.",
     "kaya-ngka kami mara bayka-ny | dog-ERG grandfather hand bite-PAST",
     "r131_affected_dog_bite_grandfather_hand", 4, js(["kaya", "kami", "mara", "baykal"]), js([]),
     "Whole-part O body affected frame modelled on Patz stepping/biting examples and prior dog bite hand row.", "0.86"),
    (38, "The dog bit the old woman's foot.",
     "Kaya-ngka mukul-jalbu jina baykan.",
     "kaya-ngka mukul-jalbu jina bayka-ny | dog-ERG old-woman foot bite-PAST",
     "r131_affected_dog_bite_old_woman_foot", 4, js(["kaya", "mukul", "jalbu", "jina", "baykal"]), js([]),
     "jina foot affected object; no track/path sense.", "0.86"),
    (39, "The crab bit the child's shin.",
     "Ngalngal-angka karrkay bibar baykan.",
     "ngalngal-angka karrkay bibar bayka-ny | crab-ERG child shin bite-PAST",
     "r131_affected_crab_bite_child_shin", 4, js(["ngalngal", "karrkay", "bibar", "baykal"]), js([]),
     "bayka-ny biting frame; bibar shin selected; common-noun A ERG.", "0.84"),
    (40, "The dog licked grandmother's face.",
     "Kaya-ngka babi walu bijan.",
     "kaya-ngka babi walu bija-ny | dog-ERG grandmother face lick-PAST",
     "r131_affected_dog_lick_grandmother_face", 4, js(["kaya", "babi", "walu", "bijal"]), js([]),
     "bija-ny lick frame from prior face rows; walu face core sense.", "0.86"),
    (41, "The dog licked father's mouth.",
     "Kaya-ngka nganjan ngandal bijan.",
     "kaya-ngka nganjan ngandal bija-ny | dog-ERG father mouth lick-PAST",
     "r131_affected_dog_lick_father_mouth", 4, js(["kaya", "nganjan", "ngandal", "bijal"]), js([]),
     "ngandal mouth core sense; whole-part O affected by lick.", "0.84"),
    (42, "The old woman poked her foot.",
     "Mukul-jalbu jina baka-jin.",
     "mukul-jalbu jina baka-ji-ny | old-woman foot poke-REF-PAST",
     "r131_reflexive_poke_old_woman_foot", 3, js(["mukul", "jalbu", "jina", "bakajil"]), js([]),
     "Directly modelled on Patz/B144 foot poke-reflexive frame; S bare.", "0.88"),
    (43, "The woman happened to step on my foot.",
     "Jalbu-ndu ngayu jina narri-jin.",
     "jalbu-ndu ngayu jina narri-ji-ny | woman-LOC:pt I foot step.on-ITR-PAST",
     "r131_step_woman_my_foot", 3, js(["jalbu", "ngayu", "jina", "narrijil"]), js([]),
     "Patz example has person-LOC:pt plus ngayu jina; jina foot sense selected.", "0.88"),
    (44, "Grandmother dropped the stone on the child's foot.",
     "Babi-ngka kulji daray-manin karrkay-anda jina-nga.",
     "babi-ngka kulji dara-y-mani-ny karrkay-anda jina-nga | grandmother-ERG stone fall-y-CAUS-PAST child-LOC:pt foot-LOC",
     "r131_drop_stone_child_foot", 4, js(["babi", "kulji", "daraymanil", "karrkay", "jina"]), js([]),
     "Patz example 521 has stone dropped on girl's foot; new possessor/agent, same sourced frame.", "0.88"),
    (45, "The woman dropped the stick on father's shin.",
     "Jalbu-ngku juku daray-manin nganjan-anda bibar-a.",
     "jalbu-ngku juku dara-y-mani-ny nganjan-anda bibar-a | woman-ERG stick fall-y-CAUS-PAST father-LOC:pt shin-LOC",
     "r131_drop_stick_father_shin", 4, js(["jalbu", "juku", "daraymanil", "nganjan", "bibar"]), js([]),
     "bibar-a shin-LOC directly sourced; drop frame retained from Patz 521.", "0.84"),
    (46, "The man dropped the stone on the old woman's back.",
     "Dingkar-angka kulji daray-manin mukul-jalbu-ndu muku-ngu.",
     "dingkar-angka kulji dara-y-mani-ny mukul-jalbu-ndu muku-ngu | man-ERG stone fall-y-CAUS-PAST old-woman-LOC:pt back-LOC",
     "r131_drop_stone_old_woman_back", 4, js(["dingkar", "kulji", "daraymanil", "mukul", "jalbu", "muku"]), js([]),
     "muku back core sense in retained body-location drop frame; kulji stone not mountain.", "0.84"),

    (47, "The old man is deaf.",
     "Mukul-dingkar milka-kari.",
     "mukul-dingkar milka-kari | old-man ear-PRIV",
     "r131_privative_old_man_deaf", 3, js(["mukul", "dingkar", "milka", "kari"]), js([]),
     "Patz example has milka-kari 'deaf/no ears'; owner changed, privative retained.", "0.88"),
    (48, "The old woman is sleepless.",
     "Mukul-jalbu miyil-dandi.",
     "mukul-jalbu miyil-dandi | old-woman eye-hard",
     "r131_compound_old_woman_sleepless", 3, js(["mukul", "jalbu", "miyil", "dandi"]), js([]),
     "Patz table gives miyil-dandi 'sleepless'; miyil eye sense selected.", "0.88"),
    (49, "The young man turned around.",
     "Warru walu-wukurrin.",
     "warru walu-wukurri-ny | young.man face-follow-PAST",
     "r131_compound_young_man_turn_around", 3, js(["warru", "walu", "wukurril"]), js([]),
     "Patz narrative gives walu-wukurri-ny 'turned around'; walu face compound retained.", "0.88"),
    (50, "The girl listened.",
     "Maral milka-janan.",
     "maral milka-jana-ny | girl ear-stand-PAST",
     "r131_compound_girl_listened", 3, js(["maral", "milka", "janay"]), js([]),
     "Patz describes milka-jana-y 'listen'; singular girl row is exact-duplicate free.", "0.86"),
]


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


def first_word(low: str) -> str:
    return low.split()[0].strip(".,!?;:\"")


def has_ergative(word: str) -> bool:
    return bool(re.search(r"-(?:ngka|angka|ngku)\b", word))


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 in enumerate(ROWS, start=1):
        seq, english, kuku, analysis, frame, _tier, words_used, loans_used, evidence, confidence = row
        label = f"{LABEL}.{seq:02d}"
        low = kuku.lower()
        english_low = english.lower()
        analysis_low = analysis.lower()
        try:
            words = json.loads(words_used)
            loans = json.loads(loans_used)
        except json.JSONDecodeError as exc:
            issues.append(f"{label}: JSON field failed: {exc}")
            words = []
            loans = []
        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("r131_"):
            issues.append(f"{label}: Rule 131 frame must start with r131_")
        if low in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface within batch")
        seen_ku.add(low)
        if loans:
            issues.append(f"{label}: Rule 131 body-part batch should not introduce loans")
        for denied in DENY:
            if has_token(low, denied):
                issues.append(f"{label}: denied or out-of-scope token {denied!r}")
        for marker in FORBIDDEN_MARKERS:
            if marker in low:
                issues.append(f"{label}: forbidden construction marker {marker}")
        if "INST" in analysis or "-bu" in analysis_low or re.search(r"\b[a-z]+-bu\b", low):
            issues.append(f"{label}: no instrument rows in Rule 131")
        if "COMIT" in analysis:
            issues.append(f"{label}: no comitative rows in Rule 131")
        if re.search(r"-(?:amu|mu|ndamun|andamun)\b", low):
            issues.append(f"{label}: no alienable possessive suffixes in Rule 131 body-part appositions")
        for head, gloss in BODY_HEADS.items():
            if has_token(low, head) and head not in words:
                issues.append(f"{label}: body head {head} missing from words_used")
            if has_token(low, head) and gloss not in analysis_low and not (
                head == "milka" and "ear-" in analysis_low
            ) and not (head == "miyil" and "eye-" in analysis_low) and not (
                head == "walu" and "face-" in analysis_low
            ):
                issues.append(f"{label}: {head} must be glossed with selected body-part sense {gloss!r}")
        if has_token(low, "jina") and ("track" in english_low or "track" in analysis_low):
            issues.append(f"{label}: jina must be foot only, not track/path")
        if has_token(low, "miyil") and "tree" in analysis_low:
            issues.append(f"{label}: miyil tree sense is excluded")
        if has_token(low, "bibar") and "ankle" in analysis_low:
            issues.append(f"{label}: bibar must use shin sense in Rule 131")
        if has_token(low, "kaka") and not any(word in analysis_low for word in ("sore", "pain")):
            issues.append(f"{label}: kaka must be glossed sore/pain")
        first = first_word(low)
        if any(verb in low for verb in TRANSITIVE_PAST):
            if first not in PRONOUN_A and not has_ergative(first):
                issues.append(f"{label}: common-noun A in transitive past frame should be ERG-marked")
        if "nyajin" in low and first not in PRONOUN_A and not has_ergative(first):
            issues.append(f"{label}: common-noun perceiver with nyaji-l should be ERG-marked")
        if "julurri-jin" in low and has_ergative(first):
            issues.append(f"{label}: reflexive wash S should not be ERG-marked")
        if "baka-jin" in low and has_ergative(first):
            issues.append(f"{label}: reflexive poke S should not be ERG-marked")
        if "narri-jin" in low and not re.search(r"^[a-z-]+-(?:anda|ndu|nda|anka)\b", low):
            issues.append(f"{label}: narri-ji stepping frame should keep person LOC:pt trigger")
        if any(loc in low for loc in ("mara-nga", "jina-nga", "muku-ngu", "ngandal-ba", "bibar-a")):
            if "LOC" not in analysis:
                issues.append(f"{label}: body-part location row must show LOC analysis")
        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 sentences WHERE batch_id=?", (batch_id,))
        db.execute("UPDATE batches SET notes=?, created_at=? WHERE id=?", ("rebanked under_review", NOW, batch_id))
    else:
        cur = db.execute(
            "INSERT INTO batches(label, cheatsheet_version, guide_version, notes, created_at) VALUES(?,?,?,?,?)",
            (LABEL, "Rule 131 draft: body-part lexical expansion", "operator-guide-2026-07-07", "under_review", NOW),
        )
        batch_id = cur.lastrowid

    dupes: list[str] = []
    for seq, _english, kuku, *_rest in ROWS:
        hit = db.execute(
            """
            SELECT b.label, s.seq
            FROM sentences s JOIN batches b ON b.id=s.batch_id
            WHERE lower(trim(s.kuku))=lower(trim(?))
              AND s.status!='rejected'
              AND s.batch_id!=?
            """,
            (kuku, batch_id),
        ).fetchone()
        if hit:
            dupes.append(f"{LABEL}.{seq:02d} duplicates {hit[0]}.{hit[1]:02d}: {kuku}")
    if dupes:
        db.rollback()
        print("cross-corpus duplicate check failed:")
        for dupe in dupes:
            print(f"  - {dupe}")
        return 1

    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="source-pass",
        actor="claude-main",
        detail=(
            "Rule 131 source pass checked dictionary.yaml exact entries, Patz grammar body-part examples, existing "
            "lexeme-table rows, and prior corpus surfaces before authoring 50 hand-written body-part rows."
        ),
        command="rg/sed dictionary.yaml and grammar.md; sqlite lexeme readback; exact duplicate scan over planned rows",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="dictionary-source-check",
        actor="claude-main",
        detail=(
            "Dictionary controls: mara hand, jina foot, walu face, milka ear, miyil eye with tree sense excluded, "
            "ngandal mouth, dukul head with extensions excluded, muku back with hawk sense excluded, bibar shin/ankle "
            "with shin selected, and kaka only as sore/pain adjective."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="grammar-source-check",
        actor="claude-main",
        detail=(
            "Grammar controls included Patz ngandal mouth table, mara free noun root for hand, body-part compounds "
            "miyil/milka/walu/mara, dukul-ku and nyulu dukul yalbay, bibar-a shin, milka-kari, miyil-dandi, "
            "mara julurri-ji-y, jina foot stepping and jina-nga, muku kaka, and walu-wukurri-ny."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Controls: no generator-authored rows; no phantom body heads; no new instruments, comitatives, alienable "
            "possessives, or temporal clitic constructions; jina foot not track/path; miyil eye not tree; bibar shin "
            "not ankle; kaka not counted as a body-part head."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="existing-surface-scan",
        actor="claude-main",
        detail="Final planned-surface scan found 50 planned rows, 50 unique within-batch surfaces, and zero exact cross-corpus duplicates.",
        command="sqlite exact-surface query over planned Batch 173 rows",
        output_excerpt="planned 50 unique 50; no duplicate lines emitted",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="process",
        event_type="process-capture-expanded",
        actor="claude-main",
        detail=(
            "Batch 173 stores source inventory, selected-sense decisions, static guards, duplicate scan, row grouping, "
            "bank outcome, and later lint/review/close/export/docs evidence as queryable process_events plus the process log."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    db.executemany(
        """
        INSERT INTO sentences(
            batch_id, seq, english, kuku, analysis, frame, tier, words_used,
            loans_used, evidence, status, confidence, rights_status, author,
            created_at, updated_at
        ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        [
            (
                batch_id, seq, english, kuku, analysis, frame, tier, words_used,
                loans_used, evidence, "under_review", confidence,
                "project_approved_synthetic_pending_elder_verification", "claude-main", NOW, NOW,
            )
            for seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, confidence in ROWS
        ],
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="bank-success",
        actor="claude-main",
        detail="Banked 50 hand-written Rule 131 body-part lexical rows as under_review after static and exact duplicate checks.",
        command="python3 batch_scripts/bank_173.py",
        output_excerpt="banked batch-173: 50 rows under_review; duplicate scan clean",
        artifact_path="batch_scripts/bank_173.py",
        created_at=NOW,
    )
    db.commit()
    count = db.execute("SELECT COUNT(*) FROM sentences WHERE batch_id=?", (batch_id,)).fetchone()[0]
    under_review = db.execute("SELECT COUNT(*) FROM sentences WHERE batch_id=? AND status='under_review'", (batch_id,)).fetchone()[0]
    verified = db.execute("SELECT COUNT(*) FROM sentences WHERE status IN ('accepted','revised')").fetchone()[0]
    print(f"banked {LABEL}: rows={count}, under_review={under_review}, verified_before_close={verified}")
    return 0


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