#!/usr/bin/env python3
"""Bank batch-152: inanimate possessor, material-source, and comitative case alternations."""

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-152"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")

DENY = {
    "barka", "wankarr", "garden", "jinajina", "yalanji-jinajina", "marra-muny",
    "table", "gate", "bucket", "torch", "motorcar", "store", "wunbu-kari", "wawu-mumbu",
    "bikirr", "ngawuya",
}


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


ROWS = [
    (1, "The leaves on that tree are big.",
     "Birra yinya-nga juku-ngu yalbay.",
     "birra yinya-nga juku-ngu yalbay | leaves that-LOC tree-LOC big",
     "inanimate_possessor_locative", 5, js(["birra", "yinyanga", "juku", "yalbay"]), js([]),
     "Patz ex.524-525 license plant parts/fruit with demonstrative-plus-tree locative.", "0.86"),
    (2, "The leaves from that tree are dry.",
     "Birra yinya-muny juku-muny marral.",
     "birra yinya-muny juku-muny marral | leaves that-ABL tree-ABL dry",
     "inanimate_possessor_ablative", 5, js(["birra", "yinyamuny", "juku", "marral"]), js([]),
     "Patz ex.524 contrasts locative leaves on a tree with ablative leaves from a tree.", "0.86"),
    (3, "The flowers on the tree are wilted.",
     "Nganka juku-ngu nganjay.",
     "nganka juku-ngu nganjay | flowers tree-LOC wilted/no.good",
     "plant_part_locative", 4, js(["nganka", "juku", "nganjay"]), js([]),
     "Dictionary gives nganka nganjay 'wilted flowers'; juku-ngu supplies the plant host.", "0.80"),
    (4, "There are many plant shoots on the tree.",
     "Jurrbu juku-ngu wubul.",
     "jurrbu juku-ngu wubul | plant.shoots tree-LOC many",
     "plant_part_locative_quantity", 4, js(["jurrbu", "juku", "wubul"]), js([]),
     "Jurrbu means plant shoots; locative host follows the Rule 110 pattern.", "0.80"),
    (5, "The broad leaf from the tree lay in the bag.",
     "Bindi juku-muny balji-nga wunan.",
     "bindi juku-muny balji-nga wuna-ny | broad.leaf tree-ABL bag-LOC lie-PAST",
     "ablative_source_plus_container_locative", 4, js(["bindi", "juku", "balji", "wunay"]), js([]),
     "Shows plant origin in ABL and current container in LOC.", "0.80"),
    (6, "The branches on the tree are hard.",
     "Dakil-dakil juku-ngu dandi.",
     "dakil-dakil juku-ngu dandi | branches tree-LOC hard",
     "tree_part_locative", 4, js(["dakil-dakil", "juku", "dandi"]), js([]),
     "Dakil-dakil is branches of a tree; juku-ngu keeps the host locative.", "0.80"),
    (7, "The base of that tree is hard.",
     "Dingal yinya-nga juku-ngu dandi.",
     "dingal yinya-nga juku-ngu dandi | base that-LOC tree-LOC hard",
     "tree_part_demonstrative_locative", 5, js(["dingal", "yinyanga", "juku", "dandi"]), js([]),
     "Dingal is base/end; source pattern follows yinya-nga juku-ngu.", "0.82"),
    (8, "The canopy on that tree is big.",
     "Wakumba yinya-nga juku-ngu yalbay.",
     "wakumba yinya-nga juku-ngu yalbay | canopy that-LOC tree-LOC big",
     "tree_canopy_demonstrative_locative", 5, js(["wakumba", "yinyanga", "juku", "yalbay"]), js([]),
     "Wakumba is canopy/spreading branches; locative marks the host tree.", "0.82"),
    (9, "The log from the tree lay on the ground.",
     "Walmba juku-muny bubu-ngu wunan.",
     "walmba juku-muny bubu-ngu wuna-ny | log tree-ABL ground-LOC lie-PAST",
     "ablative_origin_plus_ground_locative", 4, js(["walmba", "juku", "bubu", "wunay"]), js([]),
     "Ablative marks source tree; locative marks current ground location.", "0.80"),
    (10, "The hollow trunk is there in the tree.",
     "Dimbirr juku-ngu yinyay.",
     "dimbirr juku-ngu yinyay | hollow.trunk tree-LOC there",
     "tree_part_locative", 4, js(["dimbirr", "juku", "yinyay"]), js([]),
     "Dimbirr is hollow trunk of a tree; locative is retained in the analysis.", "0.80"),
    (11, "The tree stump lay on the ground.",
     "Jakalkulu bubu-ngu wunan.",
     "jakalkulu bubu-ngu wuna-ny | tree.stump ground-LOC lie-PAST",
     "object_location_locative", 4, js(["jakalkulu", "bubu", "wunay"]), js([]),
     "Replacement for the earlier yinyay row; trains same locative with a posture predicate.", "0.80"),
    (12, "That broad leaf lay in the bag.",
     "Bindi yinya balji-nga wunan.",
     "bindi yinya balji-nga wuna-ny | broad.leaf that bag-LOC lie-PAST",
     "container_location_locative", 4, js(["bindi", "yinya", "balji", "wunay"]), js([]),
     "Replacement after duplicate preflight; yinya selects the leaf without changing the balji-nga frame.", "0.78"),
    (13, "Leichhardt fruit on that tree is good to eat.",
     "Mayi kabal yinya-nga juku-ngu ngulkurr nukaji.",
     "mayi kabal yinya-nga juku-ngu ngulkurr nuka-ji-y | fruit Leichhardt.tree that-LOC tree-LOC good eat-ITR-NONPAST",
     "fruit_on_tree_good_to_eat", 5, js(["mayi", "kabal", "yinyanga", "juku", "ngulkurr", "nukal"]), js([]),
     "Patz ex.525 good-to-eat frame; kabal mayi selects Leichhardt fruit.", "0.84"),
    (14, "Leichhardt fruit from that tree is good to eat.",
     "Mayi kabal yinya-muny juku-muny ngulkurr nukaji.",
     "mayi kabal yinya-muny juku-muny ngulkurr nuka-ji-y | fruit Leichhardt.tree that-ABL tree-ABL good eat-ITR-NONPAST",
     "fruit_from_tree_good_to_eat", 5, js(["mayi", "kabal", "yinyamuny", "juku", "ngulkurr", "nukal"]), js([]),
     "Ablative counterpart to row 13; source ex.525 allows on/from distinction.", "0.84"),
    (15, "The shell of the zamia nut is hard.",
     "Dumbul marra-mu dandi.",
     "dumbul marra-mu dandi | shell/hard.covering zamia.nut-POSS hard",
     "inanimate_possessor_possessive", 5, js(["dumbul", "marra", "dandi"]), js([]),
     "Patz ex.526 allows possessive marra-mu for the zamia shell frame.", "0.86"),
    (16, "That shell on the zamia nut is hard.",
     "Yinya dumbul marra-nga dandi.",
     "yinya dumbul marra-nga dandi | that shell/hard.covering zamia.nut-LOC hard",
     "inanimate_possessor_locative_no_ablative", 5, js(["yinya", "dumbul", "marra", "dandi"]), js([]),
     "Patz ex.526 accepts marra-nga but rejects marra-muny for this shell frame.", "0.86"),

    (17, "That water is there in the bark container.",
     "Bana yinya dubal-ba yinyay.",
     "bana yinya dubal-ba yinyay | water that bark.container-LOC there",
     "container_locative", 4, js(["bana", "yinya", "dubal", "yinyay"]), js([]),
     "Variation on the Text 51 dubal-ba water frame, with demonstrative added to avoid duplicate surface.", "0.80"),
    (18, "I got water from the bark container.",
     "Ngayu bana dubal-muny manin.",
     "ngayu bana dubal-muny mani-ny | I water bark.container-ABL get-PAST",
     "container_ablative_source", 4, js(["ngayu", "bana", "dubal", "manil"]), js([]),
     "Ablative marks source container, contrasting with row 17 locative.", "0.78"),
    (19, "There are many seeds in the bag.",
     "Diburr balji-nga wubul.",
     "diburr balji-nga wubul | seeds bag-LOC many",
     "container_locative_quantity", 4, js(["diburr", "balji", "wubul"]), js([]),
     "Diburr seed sense selected by plant/material context; balji-nga is container location.", "0.78"),
    (20, "I got seeds from the bag.",
     "Ngayu diburr balji-muny manin.",
     "ngayu diburr balji-muny mani-ny | I seeds bag-ABL get-PAST",
     "container_ablative_source", 4, js(["ngayu", "diburr", "balji", "manil"]), js([]),
     "Contrasts current containment in row 19 with source-from-bag ABL.", "0.78"),
    (21, "The rope lay there in the sack.",
     "Karra kurmun-ba yinyay wunan.",
     "karra kurmun-ba yinyay wuna-ny | rope sack-LOC there lie-PAST",
     "container_locative", 4, js(["karra", "kurmun", "yinyay", "wunay"]), js([]),
     "Kurmun-ba uses the consonant-stem locative container allomorph.", "0.80"),
    (22, "I got the rope from the sack.",
     "Ngayu karra kurmun-muny manin.",
     "ngayu karra kurmun-muny mani-ny | I rope sack-ABL get-PAST",
     "container_ablative_source", 4, js(["ngayu", "karra", "kurmun", "manil"]), js([]),
     "ABL marks the sack as source, not current location.", "0.78"),
    (23, "The plate is there in the house.",
     "Bulin bayan-ba yinyay.",
     "bulin bayan-ba yinyay | plate house-LOC there",
     "object_location_locative", 4, js(["bulin", "bayan", "yinyay"]), js(["bulin"]),
     "Bulin is a post-contact plate term; row trains object location only.", "0.76"),
    (24, "I got the plate from the house.",
     "Ngayu bulin bayan-muny manin.",
     "ngayu bulin bayan-muny mani-ny | I plate house-ABL get-PAST",
     "object_ablative_source", 4, js(["ngayu", "bulin", "bayan", "manil"]), js(["bulin"]),
     "ABL source pair for the row 23 locative.", "0.76"),
    (25, "The knife is there in the bag.",
     "Naybu balji-nga yinyay.",
     "naybu balji-nga yinyay | knife bag-LOC there",
     "object_location_locative", 4, js(["naybu", "balji", "yinyay"]), js(["naybu"]),
     "Naybu is an attested loan; kept to an object-location case contrast.", "0.76"),
    (26, "I got the knife from the bag.",
     "Ngayu naybu balji-muny manin.",
     "ngayu naybu balji-muny mani-ny | I knife bag-ABL get-PAST",
     "object_ablative_source", 4, js(["ngayu", "naybu", "balji", "manil"]), js(["naybu"]),
     "ABL source pair for row 25.", "0.76"),

    (27, "The man made a spear from black palm.",
     "Dingkar-angka kalka balkan duwar-muny.",
     "dingkar-angka kalka balka-ny duwar-muny | man-ERG:pt spear make-PAST black.palm-ABL",
     "material_source_ablative", 4, js(["dingkar", "kalka", "balkal", "duwar"]), js([]),
     "Duwar entry includes black palm and black-palm spear; -muny marks material source.", "0.78"),
    (28, "The young man made a spear from strong wood.",
     "Warru-ngku kalka balkan juku junkurr-muny.",
     "warru-ngku kalka balka-ny juku junkurr-muny | young.man-ERG spear make-PAST wood strong-ABL",
     "material_source_ablative", 4, js(["warru", "kalka", "balkal", "juku", "junkurr"]), js([]),
     "Pairs juku wood with junkurr strength as the ablative material source.", "0.78"),
    (29, "The woman made rope from kuralbi bark.",
     "Jalbu-ngku karra balkan kuralbi-muny.",
     "jalbu-ngku karra balka-ny kuralbi-muny | woman-ERG rope make-PAST kuralbi-ABL",
     "material_source_ablative", 4, js(["jalbu", "karra", "balkal", "kuralbi"]), js([]),
     "Kuralbi prose says bark from the tree was twisted into strong rope.", "0.78"),
    (30, "Grandmother wove the bag from dillybag grass.",
     "Babi-ngka balji wukurrin bilanji-muny.",
     "babi-ngka balji wukurri-ny bilanji-muny | grandmother-ERG bag weave-PAST dillybag.grass-ABL",
     "material_source_ablative", 4, js(["babi", "balji", "wukurril", "bilanji"]), js([]),
     "A-subject varied after Batch 151; -muny remains material source.", "0.80"),
    (31, "Grandmother wove the bag from coarse grass.",
     "Babi-ngka balji wukurrin mawu-muny.",
     "babi-ngka balji wukurri-ny mawu-muny | grandmother-ERG bag weave-PAST coarse.grass-ABL",
     "material_source_ablative", 4, js(["babi", "balji", "wukurril", "mawu"]), js([]),
     "Mawu is coarse grass used in bag making; row trains material-source ABL.", "0.80"),
    (32, "Grandmother wove the bag from black-palm upper fibre.",
     "Babi-ngka balji wukurrin kakan-muny.",
     "babi-ngka balji wukurri-ny kakan-muny | grandmother-ERG bag weave-PAST black.palm.upper.part-ABL",
     "material_source_ablative", 4, js(["babi", "balji", "wukurril", "kakan"]), js([]),
     "Kakan prose sense, not misleading mangrove gloss, supplies the material.", "0.78"),
    (33, "I got bark from the tree.",
     "Ngayu dubal juku-muny manin.",
     "ngayu dubal juku-muny mani-ny | I bark tree-ABL get-PAST",
     "material_source_ablative", 4, js(["ngayu", "dubal", "juku", "manil"]), js([]),
     "Dubal bark sense plus juku-muny source; not a generic bowl row.", "0.78"),
    (34, "The grass-tree resin is there on the spear.",
     "Kanunjul nganjirr-muny kalka-nga yinyay.",
     "kanunjul nganjirr-muny kalka-nga yinyay | grass.tree.resin grass.tree-ABL spear-LOC there",
     "material_source_plus_object_locative", 5, js(["kanunjul", "nganjirr", "kalka", "yinyay"]), js([]),
     "Kanunjul is resin of grass tree; kalka-nga is current location on spear.", "0.80"),

    (35, "That tree has leaves.",
     "Yinya juku birra-ji.",
     "yinya juku birra-ji | that tree leaf-COMIT",
     "attributive_comitative_zero", 5, js(["yinya", "juku", "birra"]), js([]),
     "Comitative as characteristic, parallel to Patz §4.6.1 attributive comitative.", "0.82"),
    (36, "That tree has flowers.",
     "Yinya juku nganka-ji.",
     "yinya juku nganka-ji | that tree flower-COMIT",
     "attributive_comitative_zero", 5, js(["yinya", "juku", "nganka"]), js([]),
     "Grammar has flower-COMIT tree examples; row uses zero-inflection attributive comitative.", "0.82"),
    (37, "That man has a beard.",
     "Yinya dingkar walarr-iji.",
     "yinya dingkar walarr-iji | that man beard-COMIT",
     "attributive_comitative_source", 5, js(["yinya", "dingkar", "walarr"]), js([]),
     "Patz ex.535 gives yinya dingkar walarr-iji.", "0.90"),
    (38, "That dog is very vicious.",
     "Yinya kaya kuli-ji-bajaku.",
     "yinya kaya kuli-ji-bajaku | that dog rage-COMIT-very",
     "attributive_comitative_degree", 5, js(["yinya", "kaya", "kuli"]), js([]),
     "Replacement after duplicate preflight; degree suffixes can attach to comitative adjective stems.", "0.82"),
    (39, "That man has many spears.",
     "Yinya dingkar kalka-ji wubul-ji.",
     "yinya dingkar kalka-ji wubul-ji | that man spear-COMIT many-COMIT",
     "alienable_possession_as_characteristic", 5, js(["yinya", "dingkar", "kalka", "wubul"]), js([]),
     "Replacement after duplicate preflight; follows Patz ex.537-538 alienable possession as characteristic.", "0.82"),
    (40, "I like the tree with leaves.",
     "Ngayu wawu jirray juku-ku birra-jirr-ka.",
     "ngayu wawu jirray juku-ku birra-jirr-ka | I like much tree-DAT leaf-COMIT-DAT",
     "case_stacked_comitative_dative", 5, js(["ngayu", "wawu", "jirray", "juku", "birra"]), js([]),
     "Patz ex.544 licenses juku-ku birra-jirr-ka; exact source row with jabajaba was avoided.", "0.88"),
    (41, "I like the tree with flowers.",
     "Ngayu wawu jirray juku-ku nganka-jirr-ka.",
     "ngayu wawu jirray juku-ku nganka-jirr-ka | I like much tree-DAT flower-COMIT-DAT",
     "case_stacked_comitative_dative", 4, js(["ngayu", "wawu", "jirray", "juku", "nganka"]), js([]),
     "Extends the Patz dative comitative attribute pattern from leaves to flowers.", "0.80"),
    (42, "I know the tree with fruit.",
     "Ngayu binal juku-ku mayi-jirr-ka.",
     "ngayu binal juku-ku mayi-jirr-ka | I know tree-DAT fruit-COMIT-DAT",
     "case_stacked_comitative_dative", 4, js(["ngayu", "binal", "juku", "mayi"]), js([]),
     "Patz ex.52 gives binal + DAT head plus COMIT-DAT attribute.", "0.80"),
    (43, "I know the spear with resin.",
     "Ngayu binal kalka-ka kanunjul-jirr-ka.",
     "ngayu binal kalka-ka kanunjul-jirr-ka | I know spear-DAT resin-COMIT-DAT",
     "case_stacked_comitative_dative", 4, js(["ngayu", "binal", "kalka", "kanunjul"]), js([]),
     "Dative head plus comitative-dative attribute, with culturally restrained kanunjul.", "0.78"),
    (44, "He came from the house with the tree.",
     "Nyulu kadan bayan-muny juku-jirr-muny.",
     "nyulu kada-ny bayan-muny juku-jirr-muny | he come-PAST house-ABL tree-COMIT-ABL",
     "case_stacked_comitative_ablative", 5, js(["nyulu", "kaday", "bayan", "juku"]), js([]),
     "Variant of Patz ex.546 without repeating the exact demonstrative source row.", "0.84"),
    (45, "I came from the camp with paperbark.",
     "Ngayu kadan bayan-muny buyku-jirr-muny.",
     "ngayu kada-ny bayan-muny buyku-jirr-muny | I come-PAST camp-ABL paperbark-COMIT-ABL",
     "case_stacked_comitative_ablative", 4, js(["ngayu", "kaday", "bayan", "buyku"]), js([]),
     "ABL head plus COMIT-ABL attribute; buyku paperbark is source-checked.", "0.78"),
    (46, "The vicious dog bit the child.",
     "Kaya-ngka kuli-ji-ngka karrkay baykan.",
     "kaya-ngka kuli-ji-ngka karrkay bayka-ny | dog-ERG:pt rage-COMIT-ERG:pt child bite-PAST",
     "case_stacked_comitative_ergative", 5, js(["kaya", "kuli", "karrkay", "baykal"]), js([]),
     "Patz ex.542 licenses O comitative; row trains ERG head plus COMIT-ERG attribute.", "0.82"),
    (47, "The bearded man speared the fish.",
     "Dingkar-angka walarr-iji-ngka kuyu daman.",
     "dingkar-angka walarr-iji-ngka kuyu dama-ny | man-ERG:pt beard-COMIT-ERG:pt fish spear-PAST",
     "case_stacked_comitative_ergative_source", 5, js(["dingkar", "walarr", "kuyu", "damal"]), js([]),
     "Patz ex.543 gives walarr-iji-ngka with an ERG man head.", "0.88"),
    (48, "I asked about the tree with flowers.",
     "Ngayu babajin juku-ku nganka-jirr-ka.",
     "ngayu babaji-ny juku-ku nganka-jirr-ka | I ask-PAST tree-DAT flower-COMIT-DAT",
     "case_stacked_comitative_dative_question", 4, js(["ngayu", "babajiy", "juku", "nganka"]), js([]),
     "Question/about frame uses DAT head; COMIT-DAT attribute follows Patz ex.52/544.", "0.78"),
    (49, "The woman with the bag sat in the camp.",
     "Jalbu balji-ji bayan-ba bundandan.",
     "jalbu balji-ji bayan-ba bundanda-ny | woman bag-COMIT camp-LOC sit.REDUP-PAST",
     "attributive_comitative_zero_rest", 4, js(["jalbu", "balji", "bayan", "bunday"]), js([]),
     "Zero-inflection attributive comitative with an S noun; camp location remains LOC.", "0.78"),
    (50, "The child with seeds sat there.",
     "Karrkay diburr-iji yinyay bundandan.",
     "karrkay diburr-iji yinyay bundanda-ny | child seed-COMIT there sit.REDUP-PAST",
     "attributive_comitative_zero_rest", 4, js(["karrkay", "diburr", "yinyay", "bunday"]), js([]),
     "Rhotic-final diburr takes the -iji comitative allomorph in an attributive S row.", "0.78"),
]


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}"
        e_start = english.lstrip("'\"")
        e_end = english.rstrip("'\"").rstrip()
        if not e_start[:1].isupper() or e_end[-1:] not in ".!?":
            issues.append(f"{label}: English must be capitalized and final-punctuated")
        if " | " not in analysis:
            issues.append(f"{label}: analysis missing gloss separator")
        k_start = kuku.lstrip("'\"")
        if not k_start[:1].isupper():
            issues.append(f"{label}: Kuku surface must start uppercase or with a quote before uppercase")
        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 wrong-sense/blocked token {word!r}")
        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 110 draft: inanimate possessor/material-source/comitative case alternations", "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 110 source pass reread operator-guide handoff, Rules 104/107/109, errata plant/material notes, "
            "Patz §3.2.3.2 possessive, §3.2.3.3 comitative allomorphy, §4.5.3 inanimate possessor, §4.5.5 "
            "possessive-locative relationship, §4.6.1 comitative attributes, exx.524-526 and 531-546. Dictionary "
            "checks covered every content noun, verb, adjective, and particle used in the 50 rows."
        ),
        command="rg/sed over grammar.md and dictionary.yaml; sqlite exact-surface candidate check",
        artifact_path="process_logs/batch-152-case-alternations-process.md",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Scope is constructional case alternation, not new botany. Blocked barka, wankarr, garden, unhyphenated "
            "jinajina, and marra-muny. Kept marra-muny blocked because Patz specifically rejects ablative for the "
            "zamia-shell inanimate possessor frame. Used loans bulin and naybu only in minimal object-location pairs."
        ),
        artifact_path="process_logs/batch-152-case-alternations-process.md",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="duplicate-preflight",
        actor="claude-main",
        detail=(
            "Exact-duplicate preflight rejected Bindi balji-nga wunan. (batch-151.12), Yinya kaya kuli-ji. "
            "(batch-129.21), and Yinya dingkar kalka-ji kulur-ji. (batch-130.08). Final replacements were "
            "Bindi yinya balji-nga wunan., Yinya kaya kuli-ji-bajaku., and Yinya dingkar kalka-ji wubul-ji.; "
            "the final 50-row set was duplicate-clean before banking."
        ),
        command="python3 sqlite exact-surface candidate check",
        artifact_path="process_logs/batch-152-case-alternations-process.md",
        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 110 case-alternation rows under_review after static and duplicate checks.",
        command="python3 batch_scripts/bank_152.py",
        artifact_path="batch_scripts/bank_152.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-16 cover inanimate possessor locative/ablative/possessive contrasts; 17-26 cover container/object "
            "LOC versus ABL source pairs; 27-34 cover material-source ABL plus current-location LOC; 35-50 cover "
            "zero and non-zero attributive comitative, including DAT, ABL, and ERG case-stacked forms."
        ),
        artifact_path="batch_scripts/bank_152.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())
