#!/usr/bin/env python3
"""Bank batch-150: demonstratives, spatial deixis, and place-reference paradigms."""

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

DENY = {
    "yanya", "yalay", "yalarriny", "ngawuya", "wajan", "table", "gate", "bucket", "torch",
    "motorcar", "store", "wunbu-kari", "wawu-mumbu", "bikirr",
}


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


ROWS = [
    (1, "I saw her, that woman.",
     "Ngayu nyunguny nyajin, yinya jalbu.",
     "ngayu nyunguny nyaji-ny, yinya jalbu | I her see-PAST, that woman",
     "demonstrative_apposition_source", 5, js(["ngayu", "nyunguny", "nyajil", "yinya", "jalbu"]), js([]),
     "Patz ex.29 has yinya jalbu in apposition after an object pronoun.", "0.92"),
    (2, "That child, he dreamed.",
     "Yinya karrkay nyulu bijarrin.",
     "yinya karrkay nyulu bijarri-ny | that child he dream-PAST",
     "narrative_anaphoric_demonstrative", 5, js(["yinya", "karrkay", "nyulu", "bijarril"]), js([]),
     "Patz §5.2.1.2 uses yinya karrkay to re-focus a known child in narrative.", "0.90"),
    (3, "That child went down. That one dived.",
     "Yinya karrkay bada. Yinya janjin.",
     "yinya karrkay bada. yinya janji-ny | that child down. that.one dive/swim-PAST",
     "narrative_refocus_source", 5, js(["yinya", "karrkay", "bada", "janjil"]), js([]),
     "Patz ex.627 has yinya karrkay bada. Yinya janjiny for narrative refocus.", "0.88"),
    (4, "This bird, the kookaburra, is standing there.",
     "Yanyu dikal wakuka yinyay janay.",
     "yanyu dikal wakuka yinyay jana-y | this bird kookaburra there stand-NONPAST",
     "demonstrative_np_initial", 5, js(["yanyu", "dikal", "wakuka", "yinyay", "janay"]), js([]),
     "Patz §4.1.1(d) gives yanyu dikal (wakuka) as this bird/kookaburra.", "0.86"),
    (5, "That child is sitting there.",
     "Yinya karrkay yinyay bundanday.",
     "yinya karrkay yinyay bundanda-y | that child there sit.REDUP-NONPAST",
     "demonstrative_np_local_predicate", 4, js(["yinya", "karrkay", "yinyay", "bunday"]), js([]),
     "Demonstrative pronoun yinya with local demonstrative yinyay in a simple rest predicate.", "0.82"),
    (6, "The child, that one, is sitting there.",
     "Karrkay yinya yinyay bundanday.",
     "karrkay yinya yinyay bundanda-y | child that there sit.REDUP-NONPAST",
     "postnominal_demonstrative", 4, js(["karrkay", "yinya", "yinyay", "bunday"]), js([]),
     "Patz §4.1.1(d) allows yinya karrkay or karrkay yinya with one noun.", "0.82"),
    (7, "This spear lay there.",
     "Yanyu kalka yinyay wunan.",
     "yanyu kalka yinyay wuna-ny | this spear there lie-PAST",
     "demonstrative_np_location", 4, js(["yanyu", "kalka", "yinyay", "wunay"]), js([]),
     "Initial yanyu plus noun, with separate local demonstrative yinyay.", "0.82"),
    (8, "The spear, this one, lay there.",
     "Kalka yanyu yinyay wunan.",
     "kalka yanyu yinyay wuna-ny | spear this there lie-PAST",
     "postnominal_demonstrative", 4, js(["kalka", "yanyu", "yinyay", "wunay"]), js([]),
     "Postnominal yanyu variant, preserving Patz's one-noun order flexibility.", "0.80"),
    (9, "That is good food.",
     "Yinya mayi ngulkurr.",
     "yinya mayi ngulkurr | that food good",
     "nominal_sentence_demonstrative", 5, js(["yinya", "mayi", "ngulkurr"]), js([]),
     "Patz ex.295 gives yinya mayi ngulkurr as a nominal sentence.", "0.92"),
    (10, "That food is good.",
     "Mayi yinya ngulkurr.",
     "mayi yinya ngulkurr | food that good",
     "postnominal_demonstrative_predicate", 4, js(["mayi", "yinya", "ngulkurr"]), js([]),
     "Postnominal demonstrative variant of the ex.295 nominal-sentence shape.", "0.82"),

    (11, "This woman taught me about zamia nuts.",
     "Yalu-ngku jalbu-ngku nganya binal-bungan marra-ka.",
     "yalu-ngku jalbu-ngku nganya binal-bunga-ny marra-ka | this-ERG:pt woman-ERG:pt me know-CAUS-PAST zamia.nut-DAT",
     "case_agree_ergative_source", 5, js(["yalungku", "jalbu", "nganya", "binal-bungal", "marra"]), js([]),
     "Patz ex.138: yalu-ngku agrees with jalbu-ngku in ERG:pt.", "0.94"),
    (12, "That woman cooked food.",
     "Yinya-ngka jalbu-ngku mayi wajun.",
     "yinya-ngka jalbu-ngku mayi waju-ny | that-ERG:pt woman-ERG:pt food cook-PAST",
     "case_agree_ergative", 5, js(["yinyangka", "jalbu", "mayi", "wajul"]), js([]),
     "Table 3.13 yinyangka plus a transitive A noun in ERG:pt; waju-ny surfaces wajun.", "0.86"),
    (13, "That dog bit the child.",
     "Yinya-ngka kaya-ngka karrkay baykan.",
     "yinya-ngka kaya-ngka karrkay bayka-ny | that-ERG:pt dog-ERG:pt child bite-PAST",
     "case_agree_ergative", 5, js(["yinyangka", "kaya", "karrkay", "baykal"]), js([]),
     "Same-case demonstrative-plus-noun A frame; adapted from the already controlled dog-bite pattern.", "0.86"),
    (14, "This man gave food to the child.",
     "Yalu-ngku dingkar-angka mayi dajin karrkay-anka.",
     "yalu-ngku dingkar-angka mayi daji-ny karrkay-anka | this-ERG:pt man-ERG:pt food give-PAST child-DAT:pt",
     "case_agree_ergative_dative", 4, js(["yalungku", "dingkar", "mayi", "dajil", "karrkay"]), js([]),
     "Demonstrative ERG:pt agreement with a human A noun; recipient is dative potent.", "0.82"),
    (15, "They searched for that child.",
     "Jana nubi-l-nubin yinyay-anka karrkay-anka.",
     "jana nubi-l-nubi-ny yinyay-anka karrkay-anka | they search-l-REDUP-PAST that-DAT:pt child-DAT:pt",
     "case_agree_dative_source", 5, js(["jana", "nubil", "yinyayanka", "karrkay"]), js([]),
     "Patz ex.139 has yinyay-anka karrkay-anka.", "0.94"),
    (16, "They searched for this child.",
     "Jana nubi-l-nubin yaluy-unku karrkay-anka.",
     "jana nubi-l-nubi-ny yaluy-unku karrkay-anka | they search-l-REDUP-PAST this-DAT:pt child-DAT:pt",
     "case_agree_dative_this", 4, js(["jana", "nubil", "yaluyunku", "karrkay"]), js([]),
     "Table 3.13 gives Yalanji DAT:pt this as yaluyunku; noun remains karrkay-anka.", "0.80"),
    (17, "Give the paper to that one.",
     "Kaban yinyay-anda daya!",
     "kaban yinyay-anda daya | paper that-LOC:pt give.IMP",
     "locative_person_goal_source", 5, js(["kaban", "yinyayanda", "dajil"]), js([]),
     "Patz ex.140 has yinyay-anda kaban daya; dictionary glosses kaban as paper/book.", "0.90"),
    (18, "Give the paper to this one.",
     "Kaban yaluy-undu daya!",
     "kaban yaluy-undu daya | paper this-LOC:pt give.IMP",
     "locative_person_goal_this", 4, js(["kaban", "yaluyundu", "dajil"]), js([]),
     "Table 3.13 gives Yalanji LOC:pt this as yaluyundu; source ex.140 motivates the frame.", "0.80"),
    (19, "I asked about this spear.",
     "Ngayu babajin yaluy-ngku kalka-ka.",
     "ngayu babaji-ny yaluy-ngku kalka-ka | I ask-PAST this-DAT spear-DAT",
     "dative_topic_this", 4, js(["ngayu", "babajiy", "yaluyngku", "kalka"]), js([]),
     "Patz question frames allow DAT topic; Table 3.13 gives DAT this as yaluyngku.", "0.78"),
    (20, "I asked about that spear.",
     "Ngayu babajin yinyay-ngka kalka-ka.",
     "ngayu babaji-ny yinyay-ngka kalka-ka | I ask-PAST that-DAT spear-DAT",
     "dative_topic_that", 4, js(["ngayu", "babajiy", "yinyayngka", "kalka"]), js([]),
     "Parallel DAT topic row using Table 3.13 yinyayngka for that-DAT.", "0.78"),
    (21, "This one's spear is new.",
     "Kalka yalu-mbu jirakal.",
     "kalka yalu-mbu jirakal | spear this-POSS new",
     "possessive_demonstrative_this", 4, js(["kalka", "yalumbu", "jirakal"]), js([]),
     "Table 3.13 gives POSS zero-inflection this as yalumbu.", "0.78"),
    (22, "That one's bag broke.",
     "Balji yinya-mba kadaban.",
     "balji yinya-mba kadaba-ny | bag that-POSS break-PAST",
     "possessive_demonstrative_that", 4, js(["balji", "yinyamba", "kadabal"]), js([]),
     "Table 3.13 gives POSS/LOC that as yinyamba; here possessive sense is selected.", "0.78"),

    (23, "He came from that house with the tree.",
     "Nyulu kadan bayan-muny yinya-muny juku-jirr-muny.",
     "nyulu kada-ny bayan-muny yinya-muny juku-jirr-muny | he come-PAST house-ABL that-ABL tree-COMIT-ABL",
     "ablative_demonstrative_source", 5, js(["nyulu", "kaday", "bayan", "yinyamuny", "juku"]), js([]),
     "Patz ex.546 has bayan-muny yinya-muny juku-jirr-muny.", "0.90"),
    (24, "He came from this house with the tree.",
     "Nyulu kadan bayan-muny yala-muny juku-jirr-muny.",
     "nyulu kada-ny bayan-muny yala-muny juku-jirr-muny | he come-PAST house-ABL this-ABL tree-COMIT-ABL",
     "ablative_demonstrative_this", 4, js(["nyulu", "kaday", "bayan", "yalamuny", "juku"]), js([]),
     "Table 3.13 gives yalamuny as this-ABL; frame follows Patz ex.546.", "0.80"),
    (25, "He is running from here.",
     "Nyulu yaluy-muny wanarri.",
     "nyulu yaluy-muny wanarri-y | he here-ABL run.REDUP-NONPAST",
     "local_demonstrative_ablative_source", 5, js(["nyulu", "yaluymuny", "wanarri-y"]), js([]),
     "Patz ex.141 gives nyulu yaluy-muny wanarri-y.", "0.94"),
    (26, "The child is running from there.",
     "Karrkay yinyay-muny wanarri.",
     "karrkay yinyay-muny wanarri-y | child there-ABL run.REDUP-NONPAST",
     "local_demonstrative_ablative_there", 4, js(["karrkay", "yinyaymuny", "wanarri-y"]), js([]),
     "Table 3.14 gives ABL there as yinyamuny/yinyay-muny in corpus spelling; motion frame follows ex.141.", "0.80"),
    (27, "I found many scrubhen eggs there.",
     "Ngayu jarruka wubul warrmba-bungan yinyay-mba.",
     "ngayu jarruka wubul warrmba-bunga-ny yinyay-mba | I scrubhen many uncover/find-PAST there-LOC",
     "local_demonstrative_locative_source", 5, js(["ngayu", "jarruka", "wubul", "warrmba-bungal", "yinyaymba"]), js([]),
     "Patz ex.142 gives jarruka wubul warrmba-bunga-ny yinyay-mba.", "0.92"),
    (28, "He sat there, on the rock.",
     "Nyulu yinyay-mba bundandan, kulji-nga.",
     "nyulu yinyay-mba bundanda-ny, kulji-nga | he there-LOC sit.REDUP-PAST, rock-LOC",
     "local_demonstrative_offset_location", 5, js(["nyulu", "yinyaymba", "bunday", "kulji"]), js([]),
     "Patz ex.143 prefers local demonstrative plus offset rock location.", "0.92"),
    (29, "I came to that very place.",
     "Ngayu yinyay-mba-bi kadan bubu-ngu.",
     "ngayu yinyay-mba-bi kada-ny bubu-ngu | I there-LOC-EMPH3 come-PAST place-LOC",
     "emphatic_local_demonstrative", 5, js(["ngayu", "yinyaymba", "kaday", "bubu"]), js([]),
     "Text line 30 has yinyay-mba-bi kada-ny bubu-ngu for that very place.", "0.86"),
    (30, "I came from there and turned back.",
     "Yinyay-muny ngayu kadan, nyunban.",
     "yinyay-muny ngayu kada-ny, nyunba-ny | there-ABL I come-PAST, return-PAST",
     "local_demonstrative_ablative_return", 4, js(["yinyaymuny", "ngayu", "kaday", "nyunbay"]), js([]),
     "Ablative local demonstrative with the source-backed return verb nyunbay.", "0.80"),

    (31, "Do not go with that drunkard.",
     "Kari dungay yinya-nji kamu-baka-nji.",
     "kari dunga-y yinya-nji kamu-baka-nji | NEG go-IMP that-COMIT:pt alcohol-excess-COMIT:pt",
     "comitative_demonstrative_source", 5, js(["kari", "dungay", "yinyanji", "kamu-baka"]), js([]),
     "Patz ex.90 gives kari dunga-y yinya-nji kamu-baka-nji.", "0.92"),
    (32, "I came with this dog.",
     "Ngayu yala-nji kaya-nji kadan.",
     "ngayu yala-nji kaya-nji kada-ny | I this-COMIT dog-COMIT come-PAST",
     "comitative_demonstrative_this", 4, js(["ngayu", "yalanji", "kaya", "kaday"]), js([]),
     "Table 3.13 gives yalanji as this-COMIT; kaya-nji selects companion sense.", "0.80"),
    (33, "I went hunting with that man.",
     "Ngayu yinya-nji dingkar-nji balbi-ka dungan.",
     "ngayu yinya-nji dingkar-nji balbi-ka dunga-ny | I that-COMIT man-COMIT hunt-DAT go-PAST",
     "comitative_demonstrative_that", 4, js(["ngayu", "yinyanji", "dingkar", "balbi", "dungay"]), js([]),
     "Table 3.13 yinyanji plus comitative noun, with an already controlled hunting-motion frame.", "0.80"),
    (34, "I will not go without this one.",
     "Ngayu kari dungay yalamundu.",
     "ngayu kari dunga-y yala-mundu | I NEG go-NONPAST this-ABESS",
     "abessive_demonstrative_this", 3, js(["ngayu", "kari", "dungay", "yalamundu"]), js([]),
     "Table 3.13 gives yalamundu as this-ABESS; kept simple and low-confidence.", "0.72"),
    (35, "The young man will not go without that one.",
     "Warru kari dungay yinyamundu.",
     "warru kari dunga-y yinya-mundu | young.man NEG go-NONPAST that-ABESS",
     "abessive_demonstrative_that", 3, js(["warru", "kari", "dungay", "yinyamundu"]), js([]),
     "Table 3.13 gives yinyamundu as that-ABESS; used sparingly.", "0.72"),

    (36, "These people are my kin.",
     "Yalu-rriny ngayku jawun-karra.",
     "yalu-rriny ngayku jawun-karra | these.people my kin-KPL",
     "human_plural_demonstrative_this", 5, js(["yalurriny", "ngayku", "jawun-karra"]), js([]),
     "Table 3.13 notes -rriny plural demonstratives are human-reference ABS only.", "0.86"),
    (37, "Those people are seaside people.",
     "Yinya-rriny jalun-ji-warra.",
     "yinya-rriny jalun-ji-warra | those.people sea-COMIT-inhabitant",
     "human_plural_demonstrative_that", 5, js(["yinyarriny", "jalun", "jalun-ji-warra"]), js([]),
     "Human plural demonstrative yinya-rriny with a source-backed inhabitant compound.", "0.86"),
    (38, "These people ate food.",
     "Yalu-rriny mayi nukan.",
     "yalu-rriny mayi nuka-ny | these.people food eat-PAST",
     "human_plural_demonstrative_subject", 4, js(["yalurriny", "mayi", "nukal"]), js([]),
     "Keeps -rriny in absolutive S/O position, as Patz requires.", "0.82"),
    (39, "Those people are sitting there.",
     "Yinya-rriny yinyay bundanday.",
     "yinya-rriny yinyay bundanda-y | those.people there sit.REDUP-NONPAST",
     "human_plural_demonstrative_local", 4, js(["yinyarriny", "yinyay", "bunday"]), js([]),
     "Human plural demonstrative plus separate local yinyay.", "0.82"),
    (40, "The people of this place gave food.",
     "Yaluy-arra mayi dajin.",
     "yaluy-arra mayi daji-ny | here-inhabitant food give-PAST",
     "local_inhabitant_this", 4, js(["yaluy-arra", "mayi", "dajil"]), js([]),
     "Patz §3.7 says here/there combine with -arra inhabitants.", "0.82"),
    (41, "The people of that place speared fish at the sea.",
     "Yinyay-arra jalun-ba kuyu daman.",
     "yinyay-arra jalun-ba kuyu dama-ny | there-inhabitant sea-LOC fish spear-PAST",
     "local_inhabitant_that", 4, js(["yinyay-arra", "jalun", "kuyu", "damal"]), js([]),
     "Yinyay-arra is the people of that place; sea/fish/spear frame is source-backed.", "0.82"),
    (42, "The people of this place gave food to the people of that place.",
     "Yaluy-arra-ngka yinyay-arra-nda mayi dajin.",
     "yaluy-arra-ngka yinyay-arra-nda mayi daji-ny | here-inhabitant-ERG:pt there-inhabitant-LOC:pt food give-PAST",
     "local_inhabitant_case", 4, js(["yaluy-arra", "yinyay-arra", "mayi", "dajil"]), js([]),
     "Follows Batch 80's accepted -arra case pattern, with demonstrative inhabitant forms inflecting.", "0.80"),
    (43, "The people of that place are coming here.",
     "Yinyay-arra kaday yaluy.",
     "yinyay-arra kada-y yaluy | there-inhabitant come-NONPAST here",
     "local_inhabitant_motion", 4, js(["yinyay-arra", "kaday", "yaluy"]), js([]),
     "Demonstrates yinyay-arra as a subject and yaluy as the local demonstrative goal.", "0.82"),
    (44, "How many children live there?",
     "Wanjarrinya karrkay-karrkay yinyay bundanday?",
     "wanjarrinya karrkay-karrkay yinyay bundanda-y | how.many child-REDUP there sit.REDUP-NONPAST",
     "interrogative_with_local_demonstrative", 4, js(["wanjarrinya", "karrkay", "yinyay", "bunday"]), js([]),
     "Variant of Patz ex.583; duplicate preflight rejected the exact bama source row.", "0.82"),
    (45, "Many trees are standing there beside the camp.",
     "Juku wubul yinyay janjanay bayan karangkal.",
     "juku wubul yinyay janjana-y bayan karangkal | tree many there stand.REDUP-NONPAST camp alongside",
     "local_demonstrative_with_spatial_noun", 4, js(["juku", "wubul", "yinyay", "janay", "bayan", "karangkal"]), js([]),
     "Variant of Patz ex.229 with added source-backed spatial noun; exact source row was already accepted.", "0.80"),
    (46, "Many trees are standing here.",
     "Juku wubul yaluy janjanay.",
     "juku wubul yaluy janjana-y | tree many here stand.REDUP-NONPAST",
     "local_demonstrative_here", 4, js(["juku", "wubul", "yaluy", "janay"]), js([]),
     "Here-counterpart to the Patz yinyay janjana-y tree row.", "0.82"),
    (47, "Since that time I have grown up.",
     "Wawu-yinya-muny ngayu yalbayman.",
     "wawu-yinya-muny ngayu yalbay-ma-ny | TEMP4-that-ABL I big-INCHO-PAST",
     "wawu_ablative_demonstrative_source", 5, js(["wawu-yinya-muny", "ngayu", "yalbay"]), js([]),
     "Patz ex.636 gives wawu-yinya-muny ngayu yalbay-ma-ny.", "0.94"),
    (48, "After this I learned.",
     "Wawu-yala-muny ngayu binalman.",
     "wawu-yala-muny ngayu binal-ma-ny | TEMP4-this-ABL I know-INCHO-PAST",
     "wawu_ablative_demonstrative_this", 4, js(["wawu-yala-muny", "ngayu", "binal-mal"]), js([]),
     "Patz says wawu- occurs only with ablative forms; yala-muny is Table 3.13 this-ABL.", "0.78"),
    (49, "That one is a good fisherman.",
     "Nyulu yinya mala-kuyu.",
     "nyulu yinya mala-kuyu | he that expert-fish",
     "demonstrative_predicate_person", 4, js(["nyulu", "yinya", "mala-kuyu"]), js([]),
     "Variant of Patz ex.509 yinya mala-minya; duplicate preflight rejected the exact hunter row.", "0.80"),
    (50, "That billycan is grandmother's.",
     "Jungkalu yinya kami-nga.",
     "jungkalu yinya kami-nga | billycan that grandmother-POSS",
     "nominal_sentence_demonstrative_possessor", 5, js(["jungkalu", "yinya", "kami"]), js(["jungkalu"]),
     "Patz ex.294 gives Jungkalu yinya -- kami-nga.", "0.90"),
]


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-dialect/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 108 draft: demonstratives and spatial deixis", "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 108 source pass reread Rule 47, Rules 92/94/104/106/107, errata demonstrative notes, and Patz "
            "§3.7 Tables 3.12-3.14 plus exx.29, 90, 138-143, 293-295, 509, 524-526, 583, 626-627, 636, and text "
            "line 30. Dictionary/grammar checks covered every content noun and verb used in the 50 rows."
        ),
        command="rg/sed over grammar.md and dictionary.yaml; sqlite exact-duplicate preflight",
        artifact_path="process_logs/batch-150-demonstratives-process.md",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Scope is Yalanji demonstrative/deictic grammar, not generic English this/that filler. Avoided Nyungkul "
            "forms yanya/yalay/yalarriny except as source contrast, kept yaluymbu out because Patz says Yalanji "
            "often rejects it in favor of yaluy, used abessive forms only twice at low confidence, and kept -rriny "
            "human plural forms in absolutive positions only."
        ),
        artifact_path="process_logs/batch-150-demonstratives-process.md",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="duplicate-preflight",
        actor="claude-main",
        detail=(
            "Exact-duplicate preflight checked 50 candidate Kuku surfaces. It rejected Wanjarrinya bama yinyay "
            "bundanday? (batch-130.38), Juku wubul yinyay janjanay. (batch-001.42), and Nyulu yinya mala-minya. "
            "(batch-147.09). Final replacements were duplicate-clean before banking."
        ),
        command="python3 sqlite exact-surface candidate check",
        artifact_path="process_logs/batch-150-demonstratives-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 108 demonstrative/deictic rows under_review after static and duplicate checks.",
        command="python3 batch_scripts/bank_150.py",
        artifact_path="batch_scripts/bank_150.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-10 cover absolutive demonstrative pronouns and nominal sentences; 11-22 case-agreeing ERG/DAT/"
            "LOC/POSS forms; 23-30 ablative and local-demonstrative rows; 31-35 COMIT/ABESS forms; 36-43 human "
            "plural -rriny and local -arra inhabitant forms; 44-50 interrogative/local, wawu-ABL, narrative, and "
            "source nominal-sentence rows."
        ),
        artifact_path="batch_scripts/bank_150.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())
