#!/usr/bin/env python3
"""Bank batch-233: Rule 191 perception-reference 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-233"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-233-perception-reference-lexical-expansion-after-rule190-process.md"

TARGET_HEADS = {
    "jangkuy": "chewing sound",
    "ngurma-mulkaynji": "image",
    "kankar": "clap sound",
    "nginja": "grunt sound",
    "numburr": "rattling sound",
    "yanja": "rustling noise",
    "kadil": "sharp sound",
    "kuwimbur": "whistle sound",
    "ngangkurr": "barking sound",
    "ngurma": "reflection",
}
PRONOUN_A = {"ngayu", "yundu", "jana", "ngana"}
DENY_KUKU = {
    "jadal-jadal", "jila-burra", "kubarru", "ngingkirr", "ngaru",
    "milkabu", "milka-bu", "nyumal", "nyuman", "miyil-janay", "warrmba-bungal",
    "yanday-nyajil", "ngangkurri-l", "kuwimbur yirrkay", "yirrkay", "badi",
    "kadan", "kaday", "kunjan", "ngara", "ngaral", "yakal", "dama", "daman",
    "baya", "kubu", "muyang", "muwar", "muryi", "kaya", "bulki", "juku",
    "jikan", "wungar", "miyil", "milka", "mara", "ngandal",
}
DENY_ENGLISH = (
    "loan", "english loan", "place name", "homograph", "eat", "eats", "ate", "eating",
    "food", "mouth", "hand", "body", "smell", "taste", "tasted", "tasting", "sick", "sickness",
    "pig", "dog", "motor", "machine", "vehicle", "wind", "tree", "grass", "axe", "cut",
    "cutting", "wood", "fall", "falling", "danger", "whip", "strike", "striking",
    "song", "sing", "singing", "shout", "shouting", "bird", "bone", "make", "making",
    "made", "instrument", "mirror", "water", "picture", "statue", "movie", "cards",
    "shadow spirit", "spirit", "overhear", "listen", "ngara", "plural", "case",
    "productive reduplication", "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, 4, js(words), js([]), evidence, "0.84")


ROWS = [
    row(1, "The chewing sound is here.", "Jangkuy yaluy.", "jangkuy yaluy | chewing.sound here", "r191_deictic_jangkuy_here", ["jangkuy", "yaluy"], "Dictionary head jangkuy 'sound of chewing'; sound noun only."),
    row(2, "The image is here.", "Ngurma-mulkaynji yaluy.", "ngurma-mulkaynji yaluy | image here", "r191_deictic_ngurma_mulkaynji_here", ["ngurma-mulkaynji", "yaluy"], "Dictionary head ngurma-mulkaynji 'image'; image noun only."),
    row(3, "The clap sound is here.", "Kankar yaluy.", "kankar yaluy | clap.sound here", "r191_deictic_kankar_here", ["kankar", "yaluy"], "Dictionary head kankar 'sound of a clap'; sound noun only."),
    row(4, "The grunt sound is here.", "Nginja yaluy.", "nginja yaluy | grunt.sound here", "r191_deictic_nginja_here", ["nginja", "yaluy"], "Dictionary head nginja 'grunt'; sound noun only."),
    row(5, "The rattling sound is here.", "Numburr yaluy.", "numburr yaluy | rattling.sound here", "r191_deictic_numburr_here", ["numburr", "yaluy"], "Dictionary head numburr 'sound of a motor, rattling noise'; selected as rattling sound only."),
    row(6, "The rustling noise is here.", "Yanja yaluy.", "yanja yaluy | rustling.noise here", "r191_deictic_yanja_here", ["yanja", "yaluy"], "Dictionary head yanja 'rustling noise'; no wind/tree/grass event row."),
    row(7, "The sharp sound is here.", "Kadil yaluy.", "kadil yaluy | sharp.sound here", "r191_deictic_kadil_here", ["kadil", "yaluy"], "Dictionary head kadil 'sharp/crashing sound'; no axe/cutting/tree-fall row."),
    row(8, "The whistle sound is here.", "Kuwimbur yaluy.", "kuwimbur yaluy | whistle.sound here", "r191_deictic_kuwimbur_here", ["kuwimbur", "yaluy"], "Dictionary head kuwimbur 'whistle'; selected as sound noun only."),
    row(9, "The barking sound is here.", "Ngangkurr yaluy.", "ngangkurr yaluy | barking.sound here", "r191_deictic_ngangkurr_here", ["ngangkurr", "yaluy"], "Dictionary head ngangkurr 'bark/sound of dog barking'; no dog/event row."),
    row(10, "The reflection is here.", "Ngurma yaluy.", "ngurma yaluy | reflection here", "r191_deictic_ngurma_here", ["ngurma", "yaluy"], "Dictionary head ngurma 'reflection/shadow/picture'; selected as reflection only."),
    row(11, "I hear the chewing sound.", "Ngayu jangkuy nyajil.", "ngayu jangkuy nyaji-l | I chewing.sound perceive-NONPAST", "r191_present_i_jangkuy", ["ngayu", "jangkuy", "nyaji-l"], "First-person present perception; nyaji-l covers see/hear/perceive."),
    row(12, "I see the image.", "Ngayu ngurma-mulkaynji nyajil.", "ngayu ngurma-mulkaynji nyaji-l | I image perceive-NONPAST", "r191_present_i_ngurma_mulkaynji", ["ngayu", "ngurma-mulkaynji", "nyaji-l"], "First-person present perception; image sense only."),
    row(13, "I hear the clap sound.", "Ngayu kankar nyajil.", "ngayu kankar nyaji-l | I clap.sound perceive-NONPAST", "r191_present_i_kankar", ["ngayu", "kankar", "nyaji-l"], "First-person present perception; no clapping action row."),
    row(14, "I hear the grunt sound.", "Ngayu nginja nyajil.", "ngayu nginja nyaji-l | I grunt.sound perceive-NONPAST", "r191_present_i_nginja", ["ngayu", "nginja", "nyaji-l"], "First-person present perception; no pig/dog/grunting event row."),
    row(15, "I hear the rattling sound.", "Ngayu numburr nyajil.", "ngayu numburr nyaji-l | I rattling.sound perceive-NONPAST", "r191_present_i_numburr", ["ngayu", "numburr", "nyaji-l"], "First-person present perception; no motor/machine row."),
    row(16, "I hear the rustling noise.", "Ngayu yanja nyajil.", "ngayu yanja nyaji-l | I rustling.noise perceive-NONPAST", "r191_present_i_yanja", ["ngayu", "yanja", "nyaji-l"], "First-person present perception; no wind/tree/grass row."),
    row(17, "I hear the sharp sound.", "Ngayu kadil nyajil.", "ngayu kadil nyaji-l | I sharp.sound perceive-NONPAST", "r191_present_i_kadil", ["ngayu", "kadil", "nyaji-l"], "First-person present perception; no axe/cutting/tree-fall row."),
    row(18, "I hear the whistle sound.", "Ngayu kuwimbur nyajil.", "ngayu kuwimbur nyaji-l | I whistle.sound perceive-NONPAST", "r191_present_i_kuwimbur", ["ngayu", "kuwimbur", "nyaji-l"], "First-person present perception; no whistling/song/instrument row."),
    row(19, "I hear the barking sound.", "Ngayu ngangkurr nyajil.", "ngayu ngangkurr nyaji-l | I barking.sound perceive-NONPAST", "r191_present_i_ngangkurr", ["ngayu", "ngangkurr", "nyaji-l"], "First-person present perception; no dog/event row."),
    row(20, "I see the reflection.", "Ngayu ngurma nyajil.", "ngayu ngurma nyaji-l | I reflection perceive-NONPAST", "r191_present_i_ngurma", ["ngayu", "ngurma", "nyaji-l"], "First-person present perception; reflection sense only."),
    row(21, "You hear the chewing sound there.", "Yundu jangkuy yinyay nyajil.", "yundu jangkuy yinyay nyaji-l | you chewing.sound there perceive-NONPAST", "r191_present_you_jangkuy_there", ["yundu", "jangkuy", "yinyay", "nyaji-l"], "Second-person present perception with yinyay support."),
    row(22, "You see the image there.", "Yundu ngurma-mulkaynji yinyay nyajil.", "yundu ngurma-mulkaynji yinyay nyaji-l | you image there perceive-NONPAST", "r191_present_you_ngurma_mulkaynji_there", ["yundu", "ngurma-mulkaynji", "yinyay", "nyaji-l"], "Second-person present perception; no post-contact object row."),
    row(23, "You hear the clap sound there.", "Yundu kankar yinyay nyajil.", "yundu kankar yinyay nyaji-l | you clap.sound there perceive-NONPAST", "r191_present_you_kankar_there", ["yundu", "kankar", "yinyay", "nyaji-l"], "Second-person present perception; sound noun remains bare."),
    row(24, "You hear the grunt sound there.", "Yundu nginja yinyay nyajil.", "yundu nginja yinyay nyaji-l | you grunt.sound there perceive-NONPAST", "r191_present_you_nginja_there", ["yundu", "nginja", "yinyay", "nyaji-l"], "Second-person present perception; no animal/event row."),
    row(25, "You hear the rattling sound there.", "Yundu numburr yinyay nyajil.", "yundu numburr yinyay nyaji-l | you rattling.sound there perceive-NONPAST", "r191_present_you_numburr_there", ["yundu", "numburr", "yinyay", "nyaji-l"], "Second-person present perception; no motor/machine row."),
    row(26, "You hear the rustling noise there.", "Yundu yanja yinyay nyajil.", "yundu yanja yinyay nyaji-l | you rustling.noise there perceive-NONPAST", "r191_present_you_yanja_there", ["yundu", "yanja", "yinyay", "nyaji-l"], "Second-person present perception; no wind/tree/grass row."),
    row(27, "You hear the sharp sound there.", "Yundu kadil yinyay nyajil.", "yundu kadil yinyay nyaji-l | you sharp.sound there perceive-NONPAST", "r191_present_you_kadil_there", ["yundu", "kadil", "yinyay", "nyaji-l"], "Second-person present perception; no source event row."),
    row(28, "You hear the whistle sound there.", "Yundu kuwimbur yinyay nyajil.", "yundu kuwimbur yinyay nyaji-l | you whistle.sound there perceive-NONPAST", "r191_present_you_kuwimbur_there", ["yundu", "kuwimbur", "yinyay", "nyaji-l"], "Second-person present perception; no whistling event."),
    row(29, "You hear the barking sound there.", "Yundu ngangkurr yinyay nyajil.", "yundu ngangkurr yinyay nyaji-l | you barking.sound there perceive-NONPAST", "r191_present_you_ngangkurr_there", ["yundu", "ngangkurr", "yinyay", "nyaji-l"], "Second-person present perception; no dog/source row."),
    row(30, "You see the reflection there.", "Yundu ngurma yinyay nyajil.", "yundu ngurma yinyay nyaji-l | you reflection there perceive-NONPAST", "r191_present_you_ngurma_there", ["yundu", "ngurma", "yinyay", "nyaji-l"], "Second-person present perception; no picture/statue/movie/card row."),
    row(31, "They heard the chewing sound there.", "Jana jangkuy yinyay nyajin.", "jana jangkuy yinyay nyaji-ny | they chewing.sound there perceive-PAST", "r191_past_they_jangkuy_there", ["jana", "jangkuy", "yinyay", "nyaji-l"], "Third-person plural past perception with yinyay support."),
    row(32, "They saw the image there.", "Jana ngurma-mulkaynji yinyay nyajin.", "jana ngurma-mulkaynji yinyay nyaji-ny | they image there perceive-PAST", "r191_past_they_ngurma_mulkaynji_there", ["jana", "ngurma-mulkaynji", "yinyay", "nyaji-l"], "Third-person plural past perception; image sense only."),
    row(33, "They heard the clap sound there.", "Jana kankar yinyay nyajin.", "jana kankar yinyay nyaji-ny | they clap.sound there perceive-PAST", "r191_past_they_kankar_there", ["jana", "kankar", "yinyay", "nyaji-l"], "Third-person plural past perception; no clapping event."),
    row(34, "They heard the grunt sound there.", "Jana nginja yinyay nyajin.", "jana nginja yinyay nyaji-ny | they grunt.sound there perceive-PAST", "r191_past_they_nginja_there", ["jana", "nginja", "yinyay", "nyaji-l"], "Third-person plural past perception; no animal/event row."),
    row(35, "They heard the rattling sound there.", "Jana numburr yinyay nyajin.", "jana numburr yinyay nyaji-ny | they rattling.sound there perceive-PAST", "r191_past_they_numburr_there", ["jana", "numburr", "yinyay", "nyaji-l"], "Third-person plural past perception; no motor/machine row."),
    row(36, "They heard the rustling noise there.", "Jana yanja yinyay nyajin.", "jana yanja yinyay nyaji-ny | they rustling.noise there perceive-PAST", "r191_past_they_yanja_there", ["jana", "yanja", "yinyay", "nyaji-l"], "Third-person plural past perception; no wind/tree/grass row."),
    row(37, "They heard the sharp sound there.", "Jana kadil yinyay nyajin.", "jana kadil yinyay nyaji-ny | they sharp.sound there perceive-PAST", "r191_past_they_kadil_there", ["jana", "kadil", "yinyay", "nyaji-l"], "Third-person plural past perception; no axe/cutting/tree-fall row."),
    row(38, "They heard the whistle sound there.", "Jana kuwimbur yinyay nyajin.", "jana kuwimbur yinyay nyaji-ny | they whistle.sound there perceive-PAST", "r191_past_they_kuwimbur_there", ["jana", "kuwimbur", "yinyay", "nyaji-l"], "Third-person plural past perception; no whistling/song row."),
    row(39, "They heard the barking sound there.", "Jana ngangkurr yinyay nyajin.", "jana ngangkurr yinyay nyaji-ny | they barking.sound there perceive-PAST", "r191_past_they_ngangkurr_there", ["jana", "ngangkurr", "yinyay", "nyaji-l"], "Third-person plural past perception; no dog/event row."),
    row(40, "They saw the reflection there.", "Jana ngurma yinyay nyajin.", "jana ngurma yinyay nyaji-ny | they reflection there perceive-PAST", "r191_past_they_ngurma_there", ["jana", "ngurma", "yinyay", "nyaji-l"], "Third-person plural past perception; reflection sense only."),
    row(41, "We hear the chewing sound here.", "Ngana jangkuy yaluy nyajil.", "ngana jangkuy yaluy nyaji-l | we chewing.sound here perceive-NONPAST", "r191_present_we_jangkuy_here", ["ngana", "jangkuy", "yaluy", "nyaji-l"], "Inclusive present perception with yaluy support."),
    row(42, "We see the image here.", "Ngana ngurma-mulkaynji yaluy nyajil.", "ngana ngurma-mulkaynji yaluy nyaji-l | we image here perceive-NONPAST", "r191_present_we_ngurma_mulkaynji_here", ["ngana", "ngurma-mulkaynji", "yaluy", "nyaji-l"], "Inclusive present perception; image sense only."),
    row(43, "We hear the clap sound here.", "Ngana kankar yaluy nyajil.", "ngana kankar yaluy nyaji-l | we clap.sound here perceive-NONPAST", "r191_present_we_kankar_here", ["ngana", "kankar", "yaluy", "nyaji-l"], "Inclusive present perception; sound noun remains bare."),
    row(44, "We hear the grunt sound here.", "Ngana nginja yaluy nyajil.", "ngana nginja yaluy nyaji-l | we grunt.sound here perceive-NONPAST", "r191_present_we_nginja_here", ["ngana", "nginja", "yaluy", "nyaji-l"], "Inclusive present perception; no animal/event row."),
    row(45, "We hear the rattling sound here.", "Ngana numburr yaluy nyajil.", "ngana numburr yaluy nyaji-l | we rattling.sound here perceive-NONPAST", "r191_present_we_numburr_here", ["ngana", "numburr", "yaluy", "nyaji-l"], "Inclusive present perception; no motor/machine row."),
    row(46, "We hear the rustling noise here.", "Ngana yanja yaluy nyajil.", "ngana yanja yaluy nyaji-l | we rustling.noise here perceive-NONPAST", "r191_present_we_yanja_here", ["ngana", "yanja", "yaluy", "nyaji-l"], "Inclusive present perception; no wind/tree/grass row."),
    row(47, "We hear the sharp sound here.", "Ngana kadil yaluy nyajil.", "ngana kadil yaluy nyaji-l | we sharp.sound here perceive-NONPAST", "r191_present_we_kadil_here", ["ngana", "kadil", "yaluy", "nyaji-l"], "Inclusive present perception; no source event row."),
    row(48, "We hear the whistle sound here.", "Ngana kuwimbur yaluy nyajil.", "ngana kuwimbur yaluy nyaji-l | we whistle.sound here perceive-NONPAST", "r191_present_we_kuwimbur_here", ["ngana", "kuwimbur", "yaluy", "nyaji-l"], "Inclusive present perception; no whistling/song row."),
    row(49, "We hear the barking sound here.", "Ngana ngangkurr yaluy nyajil.", "ngana ngangkurr yaluy nyaji-l | we barking.sound here perceive-NONPAST", "r191_present_we_ngangkurr_here", ["ngana", "ngangkurr", "yaluy", "nyaji-l"], "Inclusive present perception; no dog/source row."),
    row(50, "We see the reflection here.", "Ngana ngurma yaluy nyajil.", "ngana ngurma yaluy nyaji-l | we reflection here perceive-NONPAST", "r191_present_we_ngurma_here", ["ngana", "ngurma", "yaluy", "nyaji-l"], "Inclusive present perception; reflection sense only."),
]


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_target_case(low: str, head: str) -> bool:
    return re.search(rf"(?<![a-z-]){re.escape(head)}-(?: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)\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)}\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("r191_"):
            issues.append(f"{label}: Rule 191 frame must start with r191_")
        if low in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface within batch")
        seen_ku.add(low)
        if json.loads(loans_used):
            issues.append(f"{label}: no loans are admitted in Rule 191")
        for denied in DENY_KUKU:
            if has_token(low, denied):
                issues.append(f"{label}: denied perception-event/rejected-candidate token {denied!r}")
        targets = [head for head in TARGET_HEADS if has_token(low, head)]
        if len(targets) != 1:
            issues.append(f"{label}: expected exactly one Rule 191 target head, found {targets}")
        for head in targets:
            selected = TARGET_HEADS[head]
            if head not in words:
                issues.append(f"{label}: target head {head} missing from words_used")
            if selected.replace(" ", ".") not in analysis:
                issues.append(f"{label}: target head {head} must be glossed as {selected}")
            if selected not in english_low:
                issues.append(f"{label}: {head} must be translated with selected sense {selected!r}")
            if has_target_case(low, head):
                issues.append(f"{label}: target head {head} has forbidden case/compound morphology")
        if seq <= 10:
            if "nyajil" in low or "nyajin" in low or "nyaji-l" in words:
                issues.append(f"{label}: deictic rows must not use nyaji-l")
            if not has_token(low, "yaluy") or "yaluy" not in words or "here" not in english_low:
                issues.append(f"{label}: Rule 191 deictic rows require yaluy/here")
        else:
            if ("nyajin" not in low and "nyajil" not in low) or "nyaji-l" not in words:
                issues.append(f"{label}: Rule 191 perception rows must use nyaji-l")
            first = first_word(low)
            if first not in PRONOUN_A:
                issues.append(f"{label}: perception A should be an approved bare pronoun")
            if 11 <= seq <= 20 and first != "ngayu":
                issues.append(f"{label}: rows 11-20 require ngayu")
            if 21 <= seq <= 30 and (first != "yundu" or not has_token(low, "yinyay") or "there" not in english_low):
                issues.append(f"{label}: rows 21-30 require yundu + yinyay/there")
            if 31 <= seq <= 40 and (first != "jana" or not has_token(low, "yinyay") or "there" not in english_low):
                issues.append(f"{label}: rows 31-40 require jana + yinyay/there")
            if 41 <= seq <= 50 and (first != "ngana" or not has_token(low, "yaluy") or "here" not in english_low):
                issues.append(f"{label}: rows 41-50 require ngana + yaluy/here")
            if "nyajin" in low and "perceive-PAST" not in analysis:
                issues.append(f"{label}: nyajin must be glossed as perceive-PAST")
            if "nyajil" in low and "perceive-NONPAST" not in analysis:
                issues.append(f"{label}: nyajil must be glossed as perceive-NONPAST")
        if any(english_has_denied(english_low, bad) for bad in DENY_ENGLISH):
            issues.append(f"{label}: denied source-event/polysemy 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 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 191 draft: perception-reference lexical expansion", "operator-guide-2026-07-08", "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 191 source pass selected ten perception-reference dictionary heads: jangkuy chewing sound, ngurma-mulkaynji image, kankar clap sound, nginja grunt sound, numburr rattling sound, yanja rustling noise, kadil sharp sound, kuwimbur whistle sound, ngangkurr barking sound, and ngurma reflection. Rejected candidates jadal-jadal, jila-burra, kubarru, ngingkirr, and ngaru stay closed. Grammar readback confirmed nyaji-l as broad see/hear/perceive with bare pronoun A, bare absolutive target objects, nyajil nonpast, nyajin past, and yaluy/yinyay support.",
                 command="dictionary YAML readback; grammar/cheatsheet grep for nyaji-l see/hear/perceive; sqlite corpus-frequency check",
                 artifact_path=PROCESS_LOG, created_at=NOW)
    record_event(db, batch_label=LABEL, phase="scope", event_type="lexical-scope", actor="claude-main",
                 detail="Rule 191 is a bounded perception-reference lexical pass. It admits bare target nouns only and excludes sound-making events, food/eating, mouth/body-part, smell/taste verbs, pig/dog/motor/machine/vehicle, wind/tree/grass motion, axe/cutting/wood/tree-fall/danger, whip/striking, song/singing/shouting, bird-bone/making/instrument, mirror/water/picture/statue/movie/cards/shadow-spirit import, target case, compounds, plural derivation, and productive reduplication.",
                 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 found 50 planned Kuku surfaces, 50 unique within-batch surfaces, and no exact accepted/revised/under_review cross-corpus duplicates.",
                 command="sqlite exact lower(trim(kuku)) duplicate scan before banking", artifact_path="batch_scripts/bank_233.py", created_at=NOW)
    record_event(db, batch_label=LABEL, phase="preflight", event_type="static-guard-pass", actor="claude-main",
                 detail="Static guard passed: exactly one selected Rule 191 target per row, no loans, no rejected candidates, no denied source-event Kuku tokens, no target case/compound morphology, correct frame distribution, correct tense glosses, and no denied source-event/polysemy English fields.",
                 command="python3 batch_scripts/bank_233.py pre-bank scan", artifact_path="batch_scripts/bank_233.py", created_at=NOW)

    db.executemany(
        """
        INSERT INTO sentences(
          batch_id, seq, english, kuku, analysis, frame, tier, words_used,
          loans_used, evidence, status, confidence, author, created_at, updated_at
        )
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        [
            (
                batch_id, seq, english, kuku, analysis, frame, tier, words_used,
                loans_used, evidence, "under_review", confidence, "claude", NOW, NOW,
            )
            for seq, english, kuku, analysis, frame, tier, words_used,
            loans_used, evidence, confidence in ROWS
        ],
    )
    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())
