#!/usr/bin/env python3
"""Bank batch-160: narrative verb case-frame consolidation."""

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-160"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-160-narrative-verb-case-frames-process.md"

DENY = {"yah", "ngawuya", "walnga", "yala-coordination"}
DENY_STANDALONE = {"ba", "ma"}
CONSTRUCTION_CLITIC_MARKERS = ("-kuda", "-ngarr", "-ngurr", "-l-urr", "wawu-")


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


ROWS = [
    (1, "Grandmother followed the child along the road.",
     "Babi-ngka karrkay wukurrin baral-darr.",
     "babi-ngka karrkay wukurri-ny baral-darr | grandmother-ERG child follow-PAST road-PERL",
     "case_follow_object_perl", 4, js(["babi", "karrkay", "wukurril", "baral"]), js([]),
     "Patz ex.111 controls wukurri-ny with a followee; baral-darr road-PERL is source-backed.", "0.84"),
    (2, "Father followed me from behind.",
     "Nganjan-angka nganya wukurrin kudamundu.",
     "nganjan-angka nganya wukurri-ny kudamundu | father-ERG me follow-PAST behind.LOC",
     "case_follow_pronoun_object", 4, js(["nganjan", "nganya", "wukurril", "kudamundu"]), js([]),
     "Patz ex.111 controls followee plus kudamundu; object is varied to first-person ACC.", "0.84"),
    (3, "The dog followed me from behind.",
     "Kaya-ngka nganya wukurrin kudamundu.",
     "kaya-ngka nganya wukurri-ny kudamundu | dog-ERG me follow-PAST behind.LOC",
     "case_follow_animal_a", 4, js(["kaya", "nganya", "wukurril", "kudamundu"]), js([]),
     "Same source frame as ex.111; animal A keeps overt followee.", "0.82"),
    (4, "The old woman left the bag in the house.",
     "Wulbuman-angka balji bawan bayan-ba.",
     "wulbuman-angka balji bawa-ny bayan-ba | old.woman-ERG bag leave-PAST house-LOC",
     "case_leave_object_place", 4, js(["wulbuman", "balji", "bawal", "bayan"]), js([]),
     "Dictionary bawal 'leave'; Patz ex.127 contrasts bawa-nyaku with a location.", "0.82"),
    (5, "The man left the road.",
     "Dingkar-angka baral bawan.",
     "dingkar-angka baral bawa-ny | man-ERG road leave-PAST",
     "case_leave_place_object", 3, js(["dingkar", "baral", "bawal"]), js([]),
     "Bawal can take a place as the object left; baral road entry verified against dictionary/grammar.", "0.80"),
    (6, "Mother left food outside.",
     "Ngamu-ngku mayi bawan ngalkal-ba.",
     "ngamu-ngku mayi bawa-ny ngalkal-ba | mother-ERG food leave-PAST outside-LOC",
     "case_leave_object_location", 4, js(["ngamu", "mayi", "bawal", "ngalkal"]), js([]),
     "Patz ex.127 supports ngalkal-ba in the leave-outside contrast.", "0.82"),
    (7, "We turned back from the water.",
     "Ngana bana-muny nyunban.",
     "ngana bana-muny nyunba-ny | we water-ABL turn.back-PAST",
     "case_return_source", 3, js(["ngana", "bana", "nyunbay"]), js([]),
     "Patz Text 3 sources ngana nyunba-y-baja and source ABL is regular.", "0.82"),
    (8, "The children turned back at the road.",
     "Karrkay-karrkay baral-ba nyunban.",
     "karrkay-karrkay baral-ba nyunba-ny | children road-LOC turn.back-PAST",
     "case_return_location", 4, js(["karrkay", "baral", "nyunbay"]), js([]),
     "Text 3 has road-location plus turn-back context; row keeps intransitive S unmarked.", "0.80"),
    (9, "The friend came from the house and turned back.",
     "Jawun bayan-muny kadan, nyunban.",
     "jawun bayan-muny kada-ny, nyunba-ny | friend house-ABL come-PAST turn.back-PAST",
     "case_come_source_return", 4, js(["jawun", "bayan", "kaday", "nyunbay"]), js([]),
     "Patz Text 3 sources came-from plus turned-back narrative sequence.", "0.80"),
    (10, "They took the child to camp.",
     "Jana karrkay bayan-ka wundin.",
     "jana karrkay bayan-ka wundi-ny | they child camp-DAT take-PAST",
     "case_take_object_goal", 4, js(["jana", "karrkay", "bayan", "wundil"]), js([]),
     "Patz ex.106 and ex.421-423 require overt taken object; DAT goal is explicit.", "0.84"),

    (11, "Grandmother gave food to the child.",
     "Babi-ngka mayi dajin karrkay-anda.",
     "babi-ngka mayi daji-ny karrkay-anda | grandmother-ERG food give-PAST child-LOC:pt",
     "case_give_object_recipient", 4, js(["babi", "mayi", "dajil", "karrkay"]), js([]),
     "Patz ex.123 and ex.545 source LOC:pt recipients for give.", "0.86"),
    (12, "Give water to grandfather.",
     "Bana daya kami-nda!",
     "bana daya kami-nda | water give.IMP grandfather-LOC:pt",
     "case_give_imperative_recipient", 3, js(["bana", "dajil", "kami"]), js([]),
     "Patz ex.123 gives command daya plus LOC:pt recipient; recipient varied from father.", "0.84"),
    (13, "Grandmother sent a message with mother.",
     "Babi-ngka kuku ngamu-ndu yungan.",
     "babi-ngka kuku ngamu-ndu yunga-ny | grandmother-ERG message mother-LOC:pt send.with-PAST",
     "case_send_message_intermediary", 4, js(["babi", "kuku", "ngamu", "yungal"]), js([]),
     "Patz ex.273-274 control yunga-l with a LOC:pt intermediary: send food/message with someone.", "0.86"),
    (14, "I send food with mother.",
     "Ngayu mayi yungal ngamu-ndu.",
     "ngayu mayi yunga-l ngamu-ndu | I food send.with-NONPAST mother-LOC:pt",
     "case_send_food_intermediary_nonpast", 4, js(["ngayu", "mayi", "yungal", "ngamu"]), js([]),
     "Patz ex.273 directly sources mayi yunga-l with ngamu-ndu as an intermediary frame.", "0.88"),
    (15, "Grandmother spoke to the child.",
     "Babi-ngka karrkay-anda balkan.",
     "babi-ngka karrkay-anda balka-ny | grandmother-ERG child-LOC:pt speak-PAST",
     "case_speak_addressee", 4, js(["babi", "karrkay", "balkal"]), js([]),
     "Patz ex.7191 and direct-speech rows source balka-ny with LOC:pt addressee.", "0.84"),
    (16, "I spoke to father.",
     "Ngayu nganjan-anda balkan.",
     "ngayu nganjan-anda balka-ny | I father-LOC:pt speak-PAST",
     "case_speak_pronoun_a_addressee", 3, js(["ngayu", "nganjan", "balkal"]), js([]),
     "Patz ex.7191 Ngayu Chris-anda balka-ny controls the frame.", "0.86"),
    (17, "The child asked father about water.",
     "Karrkay nganjan-anda bana-ka babajin.",
     "karrkay nganjan-anda bana-ka babaji-ny | child father-LOC:pt water-DAT ask-PAST",
     "case_ask_person_topic", 4, js(["karrkay", "nganjan", "bana", "babaji"]), js([]),
     "Patz ex.264/note 13: person asked is LOC:pt and inanimate topic is DAT; asker left ABS/S.", "0.88"),
    (18, "Grandmother asked mother about the house.",
     "Babi ngamu-ndu bayan-ka babajin.",
     "babi ngamu-ndu bayan-ka babaji-ny | grandmother mother-LOC:pt house-DAT ask-PAST",
     "case_ask_person_topic", 4, js(["babi", "ngamu", "bayan", "babaji"]), js([]),
     "Same ask-about source frame; no ergative on the fixed intransitive asker.", "0.88"),
    (19, "The woman made the bag from grass.",
     "Jalbu-ngku balji balkan jikan-muny.",
     "jalbu-ngku balji balka-ny jikan-muny | woman-ERG bag make-PAST grass-ABL",
     "case_make_object_material", 4, js(["jalbu", "balji", "balkal", "jikan"]), js([]),
     "Patz ex.413 has bag-making; ex.283/284 support ABL material-source in making/weaving.", "0.82"),
    (20, "The man made a spear from wood.",
     "Dingkar-angka kalka balkan juku-muny.",
     "dingkar-angka kalka balka-ny juku-muny | man-ERG spear make-PAST wood-ABL",
     "case_make_object_material", 4, js(["dingkar", "kalka", "balkal", "juku"]), js([]),
     "Patz ex.284 controls make-from material source; kalka and juku are verified lexical heads.", "0.82"),

    (21, "I saw a pig in the scrub.",
     "Ngayu bikibiki nyajin madja-nga.",
     "ngayu bikibiki nyaji-ny madja-nga | I pig see-PAST scrub-LOC",
     "case_perceive_object_location", 4, js(["ngayu", "bikibiki", "nyajil", "madja"]), js([]),
     "Dictionary bikibiki and madja; nyaji-l perception object is overt.", "0.82"),
    (22, "The child heard barking from the house.",
     "Karrkay-ngku ngangkurr bayan-muny nyajin.",
     "karrkay-ngku ngangkurr bayan-muny nyaji-ny | child-ERG barking.noise house-ABL hear-PAST",
     "case_hear_sound_source", 4, js(["karrkay", "ngangkurr", "bayan", "nyajil"]), js([]),
     "Dictionary ngangkurr barking-noise; common-noun perceiver is marked ERG and ABL source is explicit.", "0.84"),
    (23, "The child hears a song from far away.",
     "Karrkay-ngku wulngku kalakalbay-mundu nyajil.",
     "karrkay-ngku wulngku kalakalbay-mundu nyaji-l | child-ERG song far.away-ABESS hear-NONPAST",
     "case_hear_song_source_nonpast", 4, js(["karrkay", "wulngku", "kalakalbay", "nyajil"]), js([]),
     "Patz ex.109 directly controls wulngku kalakalbay-mundu nyaji-l; common-noun A is marked ERG.", "0.88"),
    (24, "I saw grandmother near the fire.",
     "Ngayu babi nyajin baya ngundu-ba.",
     "ngayu babi nyaji-ny baya ngundu-ba | I grandmother see-PAST fire near-LOC",
     "case_see_object_location", 4, js(["ngayu", "babi", "nyajil", "baya", "ngundu"]), js([]),
     "Nyaji-l object is overt; baya ngundu-ba near-fire location is source-backed.", "0.82"),
    (25, "The old people listened in the house.",
     "Mukul-bama bayan-ba milka-janan.",
     "mukul-bama bayan-ba milka-jana-ny | old-people house-LOC listen-PAST",
     "case_listen_intransitive_location", 4, js(["mukul-bama", "bayan", "milka-janay"]), js([]),
     "Dictionary/Patz source milka-janay listen; no object invented.", "0.84"),
    (26, "The young man laughed at father.",
     "Warru nganjan-anka yajarrin.",
     "warru nganjan-anka yajarri-ny | young.man father-DAT:pt laugh-PAST",
     "case_laugh_target", 3, js(["warru", "nganjan", "yajarril"]), js([]),
     "Patz Text 3 sources yajarri-ny with DAT:pt laugh targets such as kaykay-anka; subject left ABS/S.", "0.84"),
    (27, "Grandmother laughed at the children.",
     "Babi kaykay-anka yajarrin.",
     "babi kaykay-anka yajarri-ny | grandmother children-LOC:pt laugh-PAST",
     "case_laugh_target", 3, js(["babi", "kaykay", "yajarril"]), js([]),
     "Patz Text 3 has yajarri-ny kaykay-anka; subject varied.", "0.84"),
    (28, "Father found tracks on the road.",
     "Nganjan-angka baral-ba jina warrmba-bungan.",
     "nganjan-angka baral-ba jina warrmba-bunga-ny | father-ERG road-LOC track find-CAUS-PAST",
     "case_find_object_location", 4, js(["nganjan", "baral", "jina", "warrmba-bungal"]), js([]),
     "Patz ex.142 and text lines source warrmba-bunga-ny find with an overt object.", "0.84"),
    (29, "The two found fish at the river.",
     "Bula wawubaja-nga kuyu warrmba-bungan.",
     "bula wawubaja-nga kuyu warrmba-bunga-ny | they.two river-LOC fish find-CAUS-PAST",
     "case_find_object_location", 4, js(["bula", "wawubaja", "kuyu", "warrmba-bungal"]), js([]),
     "Appendix text has Bula ... mayi warrmba-bunga-ny; object/location varied.", "0.82"),
    (30, "The woman found the bag in the house.",
     "Jalbu-ngku balji warrmba-bungan bayan-ba.",
     "jalbu-ngku balji warrmba-bunga-ny bayan-ba | woman-ERG bag find-CAUS-PAST house-LOC",
     "case_find_object_location", 4, js(["jalbu", "balji", "warrmba-bungal", "bayan"]), js([]),
     "Find verb kept transitive with overt object and locative search place.", "0.82"),

    (31, "At daybreak the child got up.",
     "Muduwajuny karrkay wandin.",
     "muduwaju-ny karrkay wandi-ny | daybreak-TIME child get.up-PAST",
     "case_wake_intransitive_time", 3, js(["muduwaju", "karrkay", "wandil"]), js([]),
     "Dictionary wandil and Patz text line source get up/come out.", "0.84"),
    (32, "The woman came out of the house.",
     "Jalbu bayan-muny wandin.",
     "jalbu bayan-muny wandi-ny | woman house-ABL come.out-PAST",
     "case_emerge_source", 3, js(["jalbu", "bayan", "wandil"]), js([]),
     "Patz text line Wunkuny-ku nyulu wandi-ny sources emerge/come out; source ABL explicit.", "0.84"),
    (33, "The old people stayed at camp.",
     "Mukul-bama bayan-ba bundan.",
     "mukul-bama bayan-ba bunda-ny | old-people camp-LOC sit/stay-PAST",
     "case_stay_location", 4, js(["mukul-bama", "bayan", "bunday"]), js([]),
     "Patz posture section sources bunda-ny/bundanda-y; locative camp is overt.", "0.82"),
    (34, "We slept near the fire.",
     "Ngana baya ngundu-ba warngku wunan.",
     "ngana baya ngundu-ba warngku wuna-ny | we fire near-LOC sleep lie-PAST",
     "case_sleep_location", 4, js(["ngana", "baya", "ngundu", "warngku-wunay"]), js([]),
     "Warngku wuna-y sleep compound; near-fire location is source-backed.", "0.84"),
    (35, "Rain fell on the road.",
     "Kaba baral-ba daran.",
     "kaba baral-ba dara-ny | rain road-LOC fall-PAST",
     "case_weather_inanimate_location", 3, js(["kaba", "baral", "daray"]), js([]),
     "Weather subject is inanimate ABS; road locative is explicit.", "0.82"),
    (36, "Wind came from the mountain.",
     "Muyar manjal-muny kadan.",
     "muyar manjal-muny kada-ny | wind mountain-ABL come-PAST",
     "case_weather_source", 3, js(["muyar", "manjal", "kaday"]), js([]),
     "Weather-event kada-y with ABL source; muyar and manjal verified.", "0.82"),
    (37, "In the morning a thunderstorm came.",
     "Wunkuwingku jarramali kadan.",
     "wunkuwingku jarramali kada-ny | morning thunderstorm come-PAST",
     "case_weather_time", 3, js(["wunkuwingku", "jarramali", "kaday"]), js([]),
     "Patz ex.164 and weather rows source jarramali plus kada-y.", "0.82"),
    (38, "The water rose in the river.",
     "Wawubaja-nga bana yalbayman.",
     "wawubaja-nga bana yalbay-ma-ny | river-LOC water big-INCHO-PAST",
     "case_water_rise_location", 3, js(["wawubaja", "bana", "yalbaymal"]), js([]),
     "Prior source-backed bana yalbay-ma-ny water-rise frame; locative river added.", "0.82"),
    (39, "In the late afternoon, rain stopped.",
     "Yilay-yilay kaba wulan.",
     "yilay-yilay kaba wula-ny | late.afternoon rain stop/die-PAST",
     "case_weather_stop_time", 3, js(["yilay-yilay", "kaba", "wulay"]), js([]),
     "Wula-y stop/die for rain-stop is already source-controlled; time word varied.", "0.78"),
    (40, "The ground is dry today.",
     "Bubu marral nyiku.",
     "bubu marral nyiku | ground dry today",
     "case_weather_verbless_time", 3, js(["bubu", "marral", "nyiku"]), js([]),
     "Patz Text 3 has bubu marral; temporal position varied.", "0.80"),

    (41, "The child asked grandmother about water.",
     "Karrkay babi-nda bana-ka babajin.",
     "karrkay babi-nda bana-ka babaji-ny | child grandmother-LOC:pt water-DAT ask-PAST",
     "case_ask_person_topic", 4, js(["karrkay", "babi", "bana", "babaji"]), js([]),
     "Ask-about contrast row: person LOC:pt, topic DAT, no ergative asker.", "0.88"),
    (42, "Grandmother gave water to the child.",
     "Babi-ngka bana dajin karrkay-anda.",
     "babi-ngka bana daji-ny karrkay-anda | grandmother-ERG water give-PAST child-LOC:pt",
     "case_give_object_recipient", 4, js(["babi", "bana", "dajil", "karrkay"]), js([]),
     "Give contrast row: transitive A marked ERG, object ABS, recipient LOC:pt.", "0.86"),
    (43, "Mother sent a message with grandmother.",
     "Ngamu-ngku kuku babi-nda yungan.",
     "ngamu-ngku kuku babi-nda yunga-ny | mother-ERG message grandmother-LOC:pt send.with-PAST",
     "case_send_message_intermediary", 4, js(["ngamu", "kuku", "babi", "yungal"]), js([]),
     "Send contrast row from Patz ex.273-274; intermediary is in LOC:pt.", "0.86"),
    (44, "I spoke to grandfather.",
     "Ngayu kami-nda balkan.",
     "ngayu kami-nda balka-ny | I grandfather-LOC:pt speak-PAST",
     "case_speak_addressee", 3, js(["ngayu", "kami", "balkal"]), js([]),
     "Balka-l addressee frame; kami-nda already source-backed in prior direct-speech rows.", "0.84"),
    (45, "I asked father about camp.",
     "Ngayu nganjan-anda bayan-ka babajin.",
     "ngayu nganjan-anda bayan-ka babaji-ny | I father-LOC:pt camp-DAT ask-PAST",
     "case_ask_person_topic", 4, js(["ngayu", "nganjan", "bayan", "babaji"]), js([]),
     "Directly parallels Patz ex.264 with father and camp topic.", "0.88"),
    (46, "The child took the billycan from the house.",
     "Karrkay-ngku jungkalu bayan-muny wundin.",
     "karrkay-ngku jungkalu bayan-muny wundi-ny | child-ERG billycan house-ABL take-PAST",
     "case_take_object_source", 4, js(["karrkay", "jungkalu", "bayan", "wundil"]), js([]),
     "Wundi-l object plus source; jungkalu and bayan-muny are source-backed.", "0.82"),
    (47, "Father took water to the road.",
     "Nganjan-angka bana baral-ka wundin.",
     "nganjan-angka bana baral-ka wundi-ny | father-ERG water road-DAT take-PAST",
     "case_take_object_goal", 4, js(["nganjan", "bana", "baral", "wundil"]), js([]),
     "Patz ex.500 has take medicine road-DAT; object varied to water.", "0.84"),
    (48, "The woman left the bag outside.",
     "Jalbu-ngku balji bawan ngalkal-ba.",
     "jalbu-ngku balji bawa-ny ngalkal-ba | woman-ERG bag leave-PAST outside-LOC",
     "case_leave_object_location", 4, js(["jalbu", "balji", "bawal", "ngalkal"]), js([]),
     "Bawa-l object plus outside location; modeled on Patz ex.127 contrast.", "0.82"),
    (49, "We saw smoke from camp.",
     "Ngana bayan-muny baya kubu nyajin.",
     "ngana bayan-muny baya kubu nyaji-ny | we camp-ABL fire smoke see-PAST",
     "case_see_object_source", 4, js(["ngana", "bayan", "baya", "kubu", "nyajil"]), js([]),
     "Patz text has baya kubu nyaji-nka; ABL source makes camp source explicit.", "0.82"),
    (50, "The old people followed the children to the water.",
     "Mukul-bama-ngka karrkay-karrkay bana-ka wukurrin.",
     "mukul-bama-ngka karrkay-karrkay bana-ka wukurri-ny | old-people-ERG children water-DAT follow-PAST",
     "case_follow_object_goal", 4, js(["mukul-bama", "karrkay", "bana", "wukurril"]), js([]),
     "Followee is overt and goal is explicit; avoids bare intransitive follow.", "0.82"),
]


def scan_static() -> list[str]:
    issues: list[str] = []
    if len(ROWS) != 50:
        issues.append(f"expected 50 rows, got {len(ROWS)}")
    seqs = [r[0] for r in ROWS]
    if seqs != list(range(1, 51)):
        issues.append(f"seqs must be 1..50, got {seqs[:5]}...{seqs[-5:]}")
    seen_ku: set[str] = set()
    for row in ROWS:
        seq, english, kuku, analysis, frame, _tier, words_used, loans_used, evidence, _confidence = row
        label = f"{LABEL}.{seq:02d}"
        if not english.lstrip("'\"")[:1].isupper() or english.rstrip("'\"").rstrip()[-1:] not in ".!?":
            issues.append(f"{label}: English must be capitalized and final-punctuated")
        if not kuku.lstrip("'\"")[:1].isupper():
            issues.append(f"{label}: Kuku surface must start uppercase")
        if " | " not in analysis:
            issues.append(f"{label}: analysis missing gloss separator")
        for field_name, payload in (("words_used", words_used), ("loans_used", loans_used)):
            try:
                json.loads(payload)
            except Exception as exc:
                issues.append(f"{label}: {field_name} is not JSON: {exc}")
        low = f" {kuku.lower()} {analysis.lower()} "
        for word in DENY:
            if re.search(rf"(?<![a-z]){re.escape(word)}(?![a-z])", low):
                issues.append(f"{label}: denied blocked/special token {word!r}")
        for marker in CONSTRUCTION_CLITIC_MARKERS:
            if marker in kuku.lower() or marker in analysis.lower():
                issues.append(f"{label}: Rule 118 case-frame batch must not re-drill construction clitic marker {marker!r}")
        if "maja-nga" in kuku.lower() or "maja-nga" in analysis.lower():
            issues.append(f"{label}: new rows must normalize scrub to madja-nga; reserve maja-nga for quoted Patz text")
        for tok in re.sub(r"[.!?,]", " ", kuku.lower()).split():
            stripped = tok.strip("'\"")
            if stripped in DENY_STANDALONE:
                issues.append(f"{label}: denied standalone special interjection {stripped!r}")
            if stripped.endswith("-da"):
                issues.append(f"{label}: Rule 118 case-frame batch must not use TEMP1 -da")
        if not frame.startswith("case_"):
            issues.append(f"{label}: case-frame batch frame must start with case_")
        if kuku.lower() in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface within batch")
        seen_ku.add(kuku.lower())
    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 118 draft: narrative verb case-frame consolidation", "operator-guide-2026-07-07", "under_review", NOW),
        )
        batch_id = cur.lastrowid

    dupes = []
    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 118 source pass read Patz grammar and dictionary.yaml for narrative verb case frames: "
            "wukurri-l followee, bawa-l left object/place, wundi-l object plus source/goal, daji-l/yunga-l/balka-l "
            "LOC:pt addressees/recipients, babaji-y person LOC:pt plus topic DAT, perception objects/sources, "
            "laugh targets, find objects, wandi-l emerge, posture/rest, and inanimate weather predicates."
        ),
        command="rg/sed over grammar.md; YAML extraction over dictionary.yaml target heads",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="lexicon-source-check",
        actor="claude-main",
        detail=(
            "Confirmed dictionary-backed heads for Batch 160: babaji, balkal, bawal, dajil, nyajil, wukurril, "
            "wundil, wandil, yungal, yajarril, warrmba-bungal, warngku wunay, wunay, wulay, daray, kaday, "
            "nyunbay, milka-janay, bubu, marral, ngangkurr, bikibiki, jungkalu, jikan, manjal, ngalkal, "
            "bayan, baral-road, kudamundu, wulngku, ngundu, wawubaja, bana, baya, kaba, muyar, jarramali, "
            "wunkuwingku, yilay-yilay, ngamu, nganjan, babi, kami, karrkay, jalbu, dingkar, wulbuman, kaya, "
            "jawun, bula, jana, mayi, kuku, balji, kalka, juku, madja."
        ),
        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 recorded: ex.106 wundi-ny, ex.109 wulngku kalakalbay-mundu nyaji-l, ex.111 "
            "wukurri-ny kudamundu, ex.123 daya plus LOC:pt recipient, ex.127 wundi/bawa location contrast, "
            "ex.264 and note 13 person LOC:pt plus topic DAT for babaji-y, ex.271-274 LOC:pt receiver/send frames, "
            "ex.283/284 ABL material source, ex.377-378 fixed intransitive babaji-y, and Text 3 weather/turn-back rows."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="source-ambiguity-policy",
        actor="claude-main",
        detail=(
            "Recorded maja/madja convention: Patz Appendix Text 3 prints maja-nga glossed scrub-LOC, while "
            "dictionary.yaml has maja=boss and madja=scrub. Batch 160 normalizes new scrub rows to madja-nga and "
            "treats maja-nga as a quoted appendix spelling only."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="existing-surface-scan",
        actor="claude-main",
        detail=(
            "Checked all 50 planned Kuku surfaces against the full non-rejected corpus; no exact duplicates found. "
            "Near-neighbor rows from Batches 136-159 were used only as source-shape controls."
        ),
        command="sqlite exact-surface query over planned Batch 160 rows",
        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 new vocabulary; no Rule 116 clitics; no ergative askers for babaji-y; person addressees/recipients "
            "in LOC:pt; inanimate ask-about topics in DAT; transitive verbs keep overt objects; weather subjects are "
            "inanimate ABS; new scrub rows use madja-nga."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="process",
        event_type="process-capture-expanded",
        actor="claude-main",
        detail=(
            "Because the operator requested maximal process storage, Batch 160 records source inventory, ambiguity policy, "
            "duplicate scan, static guards, row grouping, bank result, lint/review/close/export/docs outcomes, and live "
            "verification as queryable process_events plus the narrative 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-authored Rule 118 case-frame rows under_review after static guards and duplicate checks.",
        command="python3 batch_scripts/bank_160.py",
        artifact_path="batch_scripts/bank_160.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-10 follow/leave/turn-back/take; 11-20 give/send/speak/ask/make-from; 21-30 perception/listen/laugh/find; "
            "31-40 wake/stay/sleep/weather; 41-50 mixed case contrasts."
        ),
        artifact_path="batch_scripts/bank_160.py",
        created_at=NOW,
    )
    db.commit()
    avg_words = sum(len(row[2].replace("!", " !").replace("?", " ?").split()) for row in ROWS) / len(ROWS)
    print(f"banked {LABEL}: 50 rows under_review; avg Kuku words/row={avg_words:.2f}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
