#!/usr/bin/env python3
"""Bank batch-177: Rule 135 torso/limb body-part lexical 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-177"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-177-torso-limb-body-part-lexical-expansion-process.md"

TARGET_HEADS = {
    "damu": "chest",
    "jibar": "stomach",
    "jakay": "neck",
    "binda": "shoulder",
    "wakuy": "arm",
    "wulu": "ankle",
    "wurra": "navel",
}
DENY = {
    "dakil",
    "waba",
    "jandal",
    "jibarr",
    "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", "baykan")


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


ROWS = [
    (1, "Father's chest is sore.",
     "Nganjan damu kaka.",
     "nganjan damu kaka | father chest sore",
     "r135_state_father_chest_sore", 2, js(["nganjan", "damu", "kaka"]), js([]),
     "Dictionary damu chest; Patz 4.5.2 inalienable apposition; kaka sore predicate.", "0.86"),
    (2, "Mother's chest is good.",
     "Ngamu damu ngulkurr.",
     "ngamu damu ngulkurr | mother chest good",
     "r135_state_mother_chest_good", 2, js(["ngamu", "damu", "ngulkurr"]), js([]),
     "Dictionary damu chest; ngulkurr good; no possessive suffix.", "0.86"),
    (3, "Grandmother's stomach is sore.",
     "Babi jibar kaka.",
     "babi jibar kaka | grandmother stomach sore",
     "r135_state_grandmother_stomach_sore", 2, js(["babi", "jibar", "kaka"]), js([]),
     "Dictionary jibar stomach; kaka bodily soreness only.", "0.86"),
    (4, "The child's stomach is dry.",
     "Karrkay jibar marral.",
     "karrkay jibar marral | child stomach dry",
     "r135_state_child_stomach_dry", 2, js(["karrkay", "jibar", "marral"]), js([]),
     "Dictionary jibar stomach; marral dry state predicate.", "0.84"),
    (5, "The baby's neck is sore.",
     "Ngawa jakay kaka.",
     "ngawa jakay kaka | baby neck sore",
     "r135_state_baby_neck_sore", 2, js(["ngawa", "jakay", "kaka"]), js([]),
     "Dictionary jakay back of neck/neck; appositional baby plus body part.", "0.86"),
    (6, "The young man's neck is good.",
     "Warru jakay ngulkurr.",
     "warru jakay ngulkurr | young.man neck good",
     "r135_state_young_man_neck_good", 2, js(["warru", "jakay", "ngulkurr"]), js([]),
     "Dictionary jakay neck; ngulkurr good.", "0.86"),
    (7, "The woman's shoulder is sore.",
     "Jalbu binda kaka.",
     "jalbu binda kaka | woman shoulder sore",
     "r135_state_woman_shoulder_sore", 2, js(["jalbu", "binda", "kaka"]), js([]),
     "Dictionary binda shoulder sense selected, not tree/river fork.", "0.86"),
    (8, "Grandfather's shoulder is dry.",
     "Kami binda marral.",
     "kami binda marral | grandfather shoulder dry",
     "r135_state_grandfather_shoulder_dry", 2, js(["kami", "binda", "marral"]), js([]),
     "Binda is shoulder in this row; marral dry.", "0.84"),
    (9, "The young man's arm is sore.",
     "Warru wakuy kaka.",
     "warru wakuy kaka | young.man arm sore",
     "r135_state_young_man_arm_sore", 2, js(["warru", "wakuy", "kaka"]), js([]),
     "Dictionary wakuy arm; no wakuy-mbu locative created in this lexical pass.", "0.86"),
    (10, "The old man's arm is good.",
     "Mukul-dingkar wakuy ngulkurr.",
     "mukul-dingkar wakuy ngulkurr | old-man arm good",
     "r135_state_old_man_arm_good", 2, js(["mukul", "dingkar", "wakuy", "ngulkurr"]), js([]),
     "Wakuy arm in apposition with old man; ngulkurr good.", "0.86"),
    (11, "The child's ankle is sore.",
     "Karrkay wulu kaka.",
     "karrkay wulu kaka | child ankle sore",
     "r135_state_child_ankle_sore", 2, js(["karrkay", "wulu", "kaka"]), js([]),
     "Dictionary wulu ankle/shin; row selects ankle sense.", "0.86"),
    (12, "The woman's ankle is dry.",
     "Jalbu wulu marral.",
     "jalbu wulu marral | woman ankle dry",
     "r135_state_woman_ankle_dry", 2, js(["jalbu", "wulu", "marral"]), js([]),
     "Wulu ankle selected; no new locative/dative form.", "0.84"),
    (13, "The baby's navel is good.",
     "Ngawa wurra ngulkurr.",
     "ngawa wurra ngulkurr | baby navel good",
     "r135_state_baby_navel_good", 2, js(["ngawa", "wurra", "ngulkurr"]), js([]),
     "Dictionary wurra navel/umbilical cord; row selects navel.", "0.86"),
    (14, "The child's navel is dry.",
     "Karrkay wurra marral.",
     "karrkay wurra marral | child navel dry",
     "r135_state_child_navel_dry", 2, js(["karrkay", "wurra", "marral"]), js([]),
     "Wurra navel selected; appositional body-part state.", "0.84"),

    (15, "Mother saw the child's chest.",
     "Ngamu-ngku karrkay damu nyajin.",
     "ngamu-ngku karrkay damu nyaji-ny | mother-ERG child chest see-PAST",
     "r135_perception_mother_child_chest", 4, js(["ngamu", "karrkay", "damu", "nyajil"]), js([]),
     "Nyaji-l perception frame; common-noun A ERG; damu chest.", "0.86"),
    (16, "I saw father's chest.",
     "Ngayu nganjan damu nyajin.",
     "ngayu nganjan damu nyaji-ny | I father chest see-PAST",
     "r135_perception_pronoun_father_chest", 4, js(["ngayu", "nganjan", "damu", "nyajil"]), js([]),
     "Pronoun A bare; whole-part object with damu chest.", "0.86"),
    (17, "Father saw the child's stomach.",
     "Nganjan-angka karrkay jibar nyajin.",
     "nganjan-angka karrkay jibar nyaji-ny | father-ERG child stomach see-PAST",
     "r135_perception_father_child_stomach", 4, js(["nganjan", "karrkay", "jibar", "nyajil"]), js([]),
     "Common-noun A ERG; jibar stomach, not locational jibarr.", "0.86"),
    (18, "They saw grandmother's stomach.",
     "Jana babi jibar nyajin.",
     "jana babi jibar nyaji-ny | they grandmother stomach see-PAST",
     "r135_perception_pronoun_grandmother_stomach", 4, js(["jana", "babi", "jibar", "nyajil"]), js([]),
     "Pronoun A bare; jibar stomach in appositional object.", "0.86"),
    (19, "Grandmother saw the baby's neck.",
     "Babi-ngka ngawa jakay nyajin.",
     "babi-ngka ngawa jakay nyaji-ny | grandmother-ERG baby neck see-PAST",
     "r135_perception_grandmother_baby_neck", 4, js(["babi", "ngawa", "jakay", "nyajil"]), js([]),
     "Babi-ngka ERG; jakay neck selected.", "0.86"),
    (20, "The man saw the woman's neck.",
     "Dingkar-angka jalbu jakay nyajin.",
     "dingkar-angka jalbu jakay nyaji-ny | man-ERG woman neck see-PAST",
     "r135_perception_man_woman_neck", 4, js(["dingkar", "jalbu", "jakay", "nyajil"]), js([]),
     "Common-noun A ERG; whole-part O with jakay neck.", "0.86"),
    (21, "The woman saw the man's shoulder.",
     "Jalbu-ngku dingkar binda nyajin.",
     "jalbu-ngku dingkar binda nyaji-ny | woman-ERG man shoulder see-PAST",
     "r135_perception_woman_man_shoulder", 4, js(["jalbu", "dingkar", "binda", "nyajil"]), js([]),
     "Binda shoulder selected; common-noun A ERG.", "0.86"),
    (22, "I saw grandfather's shoulder.",
     "Ngayu kami binda nyajin.",
     "ngayu kami binda nyaji-ny | I grandfather shoulder see-PAST",
     "r135_perception_pronoun_grandfather_shoulder", 4, js(["ngayu", "kami", "binda", "nyajil"]), js([]),
     "Pronoun A bare; binda shoulder sense forced in analysis.", "0.86"),
    (23, "Father saw the young man's arm.",
     "Nganjan-angka warru wakuy nyajin.",
     "nganjan-angka warru wakuy nyaji-ny | father-ERG young.man arm see-PAST",
     "r135_perception_father_young_man_arm", 4, js(["nganjan", "warru", "wakuy", "nyajil"]), js([]),
     "Wakuy arm in whole-part object; common-noun A ERG.", "0.86"),
    (24, "We saw mother's arm.",
     "Ngana ngamu wakuy nyajin.",
     "ngana ngamu wakuy nyaji-ny | we mother arm see-PAST",
     "r135_perception_pronoun_mother_arm", 4, js(["ngana", "ngamu", "wakuy", "nyajil"]), js([]),
     "Pronoun A bare; wakuy arm.", "0.86"),
    (25, "Mother saw the child's ankle.",
     "Ngamu-ngku karrkay wulu nyajin.",
     "ngamu-ngku karrkay wulu nyaji-ny | mother-ERG child ankle see-PAST",
     "r135_perception_mother_child_ankle", 4, js(["ngamu", "karrkay", "wulu", "nyajil"]), js([]),
     "Wulu ankle selected; no target case morphology.", "0.86"),
    (26, "The child saw grandmother's ankle.",
     "Karrkay-ngku babi wulu nyajin.",
     "karrkay-ngku babi wulu nyaji-ny | child-ERG grandmother ankle see-PAST",
     "r135_perception_child_grandmother_ankle", 4, js(["karrkay", "babi", "wulu", "nyajil"]), js([]),
     "Common-noun child A ERG; wulu ankle object.", "0.84"),
    (27, "I saw the baby's navel.",
     "Ngayu ngawa wurra nyajin.",
     "ngayu ngawa wurra nyaji-ny | I baby navel see-PAST",
     "r135_perception_pronoun_baby_navel", 4, js(["ngayu", "ngawa", "wurra", "nyajil"]), js([]),
     "Pronoun A bare; wurra navel selected.", "0.86"),
    (28, "Grandfather saw the child's navel.",
     "Kami-ngka karrkay wurra nyajin.",
     "kami-ngka karrkay wurra nyaji-ny | grandfather-ERG child navel see-PAST",
     "r135_perception_grandfather_child_navel", 4, js(["kami", "karrkay", "wurra", "nyajil"]), js([]),
     "Common-noun A ERG; wurra navel selected.", "0.86"),

    (29, "Mother washed the child's chest.",
     "Ngamu-ngku karrkay damu julurrin.",
     "ngamu-ngku karrkay damu julurri-ny | mother-ERG child chest wash-PAST",
     "r135_wash_mother_child_chest", 4, js(["ngamu", "karrkay", "damu", "julurril"]), js([]),
     "Transitive julurril with whole-part affected object; damu chest.", "0.86"),
    (30, "Father washed the baby's stomach.",
     "Nganjan-angka ngawa jibar julurrin.",
     "nganjan-angka ngawa jibar julurri-ny | father-ERG baby stomach wash-PAST",
     "r135_wash_father_baby_stomach", 4, js(["nganjan", "ngawa", "jibar", "julurril"]), js([]),
     "Common-noun A ERG; jibar stomach affected object.", "0.86"),
    (31, "Grandmother washed the child's neck.",
     "Babi-ngka karrkay jakay julurrin.",
     "babi-ngka karrkay jakay julurri-ny | grandmother-ERG child neck wash-PAST",
     "r135_wash_grandmother_child_neck", 4, js(["babi", "karrkay", "jakay", "julurril"]), js([]),
     "Transitive wash of affected body part; jakay neck.", "0.86"),
    (32, "The woman washed mother's shoulder.",
     "Jalbu-ngku ngamu binda julurrin.",
     "jalbu-ngku ngamu binda julurri-ny | woman-ERG mother shoulder wash-PAST",
     "r135_wash_woman_mother_shoulder", 4, js(["jalbu", "ngamu", "binda", "julurril"]), js([]),
     "Binda shoulder selected; common-noun A ERG.", "0.86"),
    (33, "The old man washed the young man's arm.",
     "Mukul-dingkar-angka warru wakuy julurrin.",
     "mukul-dingkar-angka warru wakuy julurri-ny | old-man-ERG young.man arm wash-PAST",
     "r135_wash_old_man_young_man_arm", 4, js(["mukul", "dingkar", "warru", "wakuy", "julurril"]), js([]),
     "Wakuy arm in transitive wash frame; no instrument or comitative.", "0.86"),
    (34, "Mother washed the child's ankle.",
     "Ngamu-ngku karrkay wulu julurrin.",
     "ngamu-ngku karrkay wulu julurri-ny | mother-ERG child ankle wash-PAST",
     "r135_wash_mother_child_ankle", 4, js(["ngamu", "karrkay", "wulu", "julurril"]), js([]),
     "Wulu ankle selected; affected object stays bare.", "0.86"),
    (35, "Grandmother washed the baby's navel.",
     "Babi-ngka ngawa wurra julurrin.",
     "babi-ngka ngawa wurra julurri-ny | grandmother-ERG baby navel wash-PAST",
     "r135_wash_grandmother_baby_navel", 4, js(["babi", "ngawa", "wurra", "julurril"]), js([]),
     "Wurra navel selected; ordinary transitive wash.", "0.86"),
    (36, "I washed my chest.",
     "Ngayu damu julurri-jin.",
     "ngayu damu julurri-ji-ny | I chest wash-REF-PAST",
     "r135_reflexive_wash_pronoun_chest", 3, js(["ngayu", "damu", "julurriji"]), js([]),
     "Reflexive julurriji; pronoun S bare; no possessive suffix.", "0.86"),
    (37, "The child washed his stomach.",
     "Karrkay jibar julurri-jin.",
     "karrkay jibar julurri-ji-ny | child stomach wash-REF-PAST",
     "r135_reflexive_wash_child_stomach", 3, js(["karrkay", "jibar", "julurriji"]), js([]),
     "Reflexive wash with specified affected body part; jibar stomach.", "0.84"),
    (38, "The young man washed his neck.",
     "Warru jakay julurri-jin.",
     "warru jakay julurri-ji-ny | young.man neck wash-REF-PAST",
     "r135_reflexive_wash_young_man_neck", 3, js(["warru", "jakay", "julurriji"]), js([]),
     "Reflexive wash; jakay neck; S unmarked.", "0.84"),
    (39, "The woman washed her shoulder.",
     "Jalbu binda julurri-jin.",
     "jalbu binda julurri-ji-ny | woman shoulder wash-REF-PAST",
     "r135_reflexive_wash_woman_shoulder", 3, js(["jalbu", "binda", "julurriji"]), js([]),
     "Reflexive wash; binda shoulder; no ERG on S.", "0.84"),
    (40, "I washed my ankle.",
     "Ngayu wulu julurri-jin.",
     "ngayu wulu julurri-ji-ny | I ankle wash-REF-PAST",
     "r135_reflexive_wash_pronoun_ankle", 3, js(["ngayu", "wulu", "julurriji"]), js([]),
     "Reflexive wash; wulu ankle; no possessive suffix.", "0.86"),

    (41, "The dog bit father's chest.",
     "Kaya-ngka nganjan damu baykan.",
     "kaya-ngka nganjan damu bayka-ny | dog-ERG father chest bite-PAST",
     "r135_affected_dog_bite_father_chest", 4, js(["kaya", "nganjan", "damu", "baykal"]), js([]),
     "Bayka-l bite with common-noun A ERG; affected whole-part object.", "0.84"),
    (42, "The crab bit the child's stomach.",
     "Ngalngal-angka karrkay jibar baykan.",
     "ngalngal-angka karrkay jibar bayka-ny | crab-ERG child stomach bite-PAST",
     "r135_affected_crab_bite_child_stomach", 4, js(["ngalngal", "karrkay", "jibar", "baykal"]), js([]),
     "Ngalngal crab as ERG A; jibar stomach affected object.", "0.84"),
    (43, "The dog bit the young man's arm.",
     "Kaya-ngka warru wakuy baykan.",
     "kaya-ngka warru wakuy bayka-ny | dog-ERG young.man arm bite-PAST",
     "r135_affected_dog_bite_young_man_arm", 4, js(["kaya", "warru", "wakuy", "baykal"]), js([]),
     "Wakuy arm in affected object; common-noun A ERG.", "0.84"),
    (44, "The crab bit the woman's ankle.",
     "Ngalngal-angka jalbu wulu baykan.",
     "ngalngal-angka jalbu wulu bayka-ny | crab-ERG woman ankle bite-PAST",
     "r135_affected_crab_bite_woman_ankle", 4, js(["ngalngal", "jalbu", "wulu", "baykal"]), js([]),
     "Wulu ankle selected; crab A ERG.", "0.84"),
    (45, "The dog bit grandfather's shoulder.",
     "Kaya-ngka kami binda baykan.",
     "kaya-ngka kami binda bayka-ny | dog-ERG grandfather shoulder bite-PAST",
     "r135_affected_dog_bite_grandfather_shoulder", 4, js(["kaya", "kami", "binda", "baykal"]), js([]),
     "Binda shoulder selected; no fork sense.", "0.84"),
    (46, "The crab bit the child's neck.",
     "Ngalngal-angka karrkay jakay baykan.",
     "ngalngal-angka karrkay jakay bayka-ny | crab-ERG child neck bite-PAST",
     "r135_affected_crab_bite_child_neck", 4, js(["ngalngal", "karrkay", "jakay", "baykal"]), js([]),
     "Jakay neck in affected whole-part object; common-noun A ERG.", "0.84"),
    (47, "Grandmother saw the baby's navel.",
     "Babi-ngka ngawa wurra nyajin.",
     "babi-ngka ngawa wurra nyaji-ny | grandmother-ERG baby navel see-PAST",
     "r135_perception_grandmother_baby_navel", 4, js(["babi", "ngawa", "wurra", "nyajil"]), js([]),
     "Second navel perception row with common-noun A ERG; wurra navel.", "0.86"),
    (48, "Mother washed the young man's arm.",
     "Ngamu-ngku warru wakuy julurrin.",
     "ngamu-ngku warru wakuy julurri-ny | mother-ERG young.man arm wash-PAST",
     "r135_wash_mother_young_man_arm", 4, js(["ngamu", "warru", "wakuy", "julurril"]), js([]),
     "Wakuy arm in affected wash object; common-noun A ERG.", "0.86"),
    (49, "Father's chest is dry.",
     "Nganjan damu marral.",
     "nganjan damu marral | father chest dry",
     "r135_state_father_chest_dry", 2, js(["nganjan", "damu", "marral"]), js([]),
     "Damu chest with marral dry; appositional body-part state.", "0.84"),
    (50, "The baby's ankle is good.",
     "Ngawa wulu ngulkurr.",
     "ngawa wulu ngulkurr | baby ankle good",
     "r135_state_baby_ankle_good", 2, js(["ngawa", "wulu", "ngulkurr"]), js([]),
     "Wulu ankle; ngulkurr good; no new case morphology.", "0.86"),
]


def has_base(low: str, token: str) -> bool:
    return re.search(rf"(?<![a-z]){re.escape(token)}(?:-[a-z]+)*(?![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()
        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("r135_"):
            issues.append(f"{label}: Rule 135 frame must start with r135_")
        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 135 body-part batch should not introduce loans")
        for denied in DENY:
            if has_base(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 135")
        if "COMIT" in analysis:
            issues.append(f"{label}: no comitative rows in Rule 135")
        if re.search(r"-(?:amu|mu|ndamun|andamun)\b", low):
            issues.append(f"{label}: no alienable possessive suffixes in Rule 135")
        if not any(has_base(low, head) for head in TARGET_HEADS):
            issues.append(f"{label}: Rule 135 row must exercise a selected target head")
        for head, gloss in TARGET_HEADS.items():
            if has_base(low, head):
                if head not in words:
                    issues.append(f"{label}: target head {head} missing from words_used")
                if gloss not in analysis_low:
                    issues.append(f"{label}: {head} must be glossed with selected sense {gloss!r}")
            if f"{head}-" in low:
                issues.append(f"{label}: no new target-head case form or compound for {head}")
        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 "julurri-jin" in low and has_ergative(first):
            issues.append(f"{label}: reflexive wash S should not be ERG-marked")
        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 135 draft: torso/limb 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 135 source pass checked dictionary entries for binda shoulder, damu chest, jakay neck, "
            "jibar stomach, wakuy arm, wulu ankle/shin, and wurra navel/umbilical cord; grammar anchors for "
            "inalienable body-part apposition, nyaji-l perception, julurri-l/julurri-ji-y wash/reflexive wash, "
            "and bayka-l affected-object rows; and lexeme-table readback for participants and predicates."
        ),
        command="rg/sed dictionary.yaml and grammar.md; sqlite lexeme and corpus-surface readback",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="lexical-scope",
        actor="claude-main",
        detail=(
            "Selected target heads: damu chest, jibar stomach, jakay neck, binda shoulder, wakuy arm, wulu ankle, "
            "wurra navel. Excluded waba thigh because the grammar has multiple waba-l look-up hits; excluded jandal "
            "leg to avoid reopening the leg field; excluded dakil because branch/limb ambiguity remains out of scope."
        ),
        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 target-head case forms, no instruments, no comitatives, no alienable possessive suffixes, "
            "no temporal clitics, no waba, no jandal, no dakil, no locational jibarr, common-noun A ERG, pronoun A bare, "
            "and reflexive wash S unmarked."
        ),
        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 177 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 177 stores source readback, lexical-scope decisions, static guards, duplicate scan, bank outcome, "
            "and later lint/review/close/export/docs evidence as process_events plus this 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 135 torso/limb body-part rows as under_review after static and exact duplicate checks.",
        command="python3 batch_scripts/bank_177.py",
        output_excerpt="banked batch-177: 50 rows under_review; duplicate scan clean",
        artifact_path="batch_scripts/bank_177.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())
