#!/usr/bin/env python3
"""Bank batch-153: discourse particles, interjections, and answer words."""

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

DENY = {
    "ah",
    "yah",
    "ngawuya",
    "yala-coordination",
    "walnga",
}
DENY_STANDALONE = {"ba", "ma"}


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


ROWS = [
    (1, "Maybe he will go, maybe not.",
     "Nyulu nguba dungay, nguba kari.",
     "nyulu nguba dunga-y, nguba kari | he maybe go-NONPAST maybe NEG",
     "nguba_alternative_polarity", 5, js(["nyulu", "nguba", "dungay", "kari"]), js([]),
     "Dictionary nguba example: Nyulu nguba dungay, nguba kari.", "0.90"),
    (2, "Maybe I will come back today, maybe tomorrow.",
     "Nguba ngayu nyiku kaday-baja, nguba wunkuny.",
     "nguba ngayu nyiku kada-y-baja, nguba wunkuny | maybe I today come-NONPAST-again maybe tomorrow",
     "nguba_temporal_alternatives", 5, js(["nguba", "ngayu", "nyiku", "kaday", "baja", "wunkuny"]), js([]),
     "Patz 4.9.3 ex.609 gives this nguba temporal-alternative frame.", "0.92"),
    (3, "Maybe I will stew the fish, maybe roast it.",
     "Nguba ngayu kuyu nyamal, nguba dindal.",
     "nguba ngayu kuyu nyama-l, nguba dinda-l | maybe I fish stew-NONPAST maybe roast-NONPAST",
     "nguba_predicate_alternatives", 5, js(["nguba", "ngayu", "kuyu", "nyamal", "dindal"]), js([]),
     "Patz 4.9.3 ex.610 gives the fish stew/roast alternative frame.", "0.92"),
    (4, "Where are they? Maybe they are hiding.",
     "Jana wanjabu? Nguba jana ngaki-ji.",
     "jana wanjabu, nguba jana ngaki-ji-y | they where maybe they hide-REF-NONPAST",
     "nguba_question_answer", 5, js(["jana", "wanjabu", "nguba", "ngakiji"]), js([]),
     "Patz 4.9.3 ex.608 gives the where question plus nguba hiding answer.", "0.92"),
    (5, "Maybe the child is hiding in the scrub.",
     "Nguba karrkay madja-nga ngaki-ji.",
     "nguba karrkay madja-nga ngaki-ji-y | maybe child scrub-LOC hide-REF-NONPAST",
     "nguba_location_assumption", 4, js(["nguba", "karrkay", "madja", "ngakiji"]), js([]),
     "Extends ex.608 with a source-checked locative place noun.", "0.82"),
    (6, "Maybe the rain will come tomorrow.",
     "Nguba kaba wunkuny kaday.",
     "nguba kaba wunkuny kada-y | maybe rain tomorrow come-NONPAST",
     "nguba_weather_assumption", 4, js(["nguba", "kaba", "wunkuny", "kaday"]), js([]),
     "Nguba must precede the assumption; kaba and wunkuny are established weather/time nouns.", "0.82"),
    (7, "Maybe grandmother knows where the water is.",
     "Nguba babi binal wanja bana yinyay.",
     "nguba babi binal wanja bana yinyay | maybe grandmother know where water there",
     "nguba_knowledge_assumption", 4, js(["nguba", "babi", "binal", "wanja", "bana", "yinyay"]), js([]),
     "Uses binal with a locational complement under the nguba assumption pattern.", "0.80"),
    (8, "Maybe the dog ate the meat.",
     "Nguba kaya-ngka minya nukan.",
     "nguba kaya-ngka minya nuka-ny | maybe dog-ERG meat eat-PAST",
     "nguba_past_assumption", 4, js(["nguba", "kaya", "minya", "nukal"]), js([]),
     "Tests nguba before a past-tense assumption, not only non-past alternatives.", "0.80"),
    (9, "Maybe the fish are in the deep water.",
     "Nguba kuyu bana badanga janay.",
     "nguba kuyu bana bada-nga jana-y | maybe fish water deep-LOC stand/be-NONPAST",
     "nguba_locative_assumption", 4, js(["nguba", "kuyu", "bana", "bada", "janay"]), js([]),
     "Bada-nga is a checked vowel-final locative; nguba scopes over the full locative statement.", "0.80"),
    (10, "Maybe we will stay at the camp.",
     "Nguba ngana bayan-ba bundanday.",
     "nguba ngana bayan-ba bundanda-y | maybe we.INC camp-LOC sit/stay-NONPAST",
     "nguba_stay_assumption", 4, js(["nguba", "ngana", "bayan", "bunday"]), js([]),
     "Keeps nguba clause-initial, matching Patz's statement that it precedes the assumption.", "0.82"),

    (11, "The young man is like his father.",
     "Warru yala nyungu nganjan.",
     "warru yala nyungu nganjan | young.man COMP his father",
     "yala_comparison_kin", 5, js(["warru", "yala", "nyungu", "nganjan"]), js([]),
     "Patz 4.9.3 ex.604 licenses yala followed by an absolutive NP.", "0.88"),
    (12, "The child is like her mother.",
     "Karrkay yala nyungu ngamu.",
     "karrkay yala nyungu ngamu | child COMP her mother",
     "yala_comparison_kin", 5, js(["karrkay", "yala", "nyungu", "ngamu"]), js([]),
     "Parallel to ex.604 with a different kin term; nyungu is gender-neutral possessor in context.", "0.84"),
    (13, "The dog is like a pig.",
     "Kaya yala bikibiki.",
     "kaya yala bikibiki | dog COMP pig",
     "yala_comparison_animal", 4, js(["kaya", "yala", "bikibiki"]), js([]),
     "Yala takes an absolutive comparison NP; kaya and bikibiki are source-checked animal nouns.", "0.80"),
    (14, "He looks like a pig.",
     "Nyulu yala walu yala bikibiki.",
     "nyulu yala walu yala bikibiki | he COMP face COMP pig",
     "yala_appearance_frame", 5, js(["nyulu", "yala", "walu", "bikibiki"]), js([]),
     "Patz 3.9.2 ex.205 gives the yala walu yala bikibiki appearance frame.", "0.90"),
    (15, "I like fruit like mango.",
     "Ngayu wawu jirray mayi-ka yala mayngku.",
     "ngayu wawu jirray mayi-ka yala mayngku | I like much fruit-DAT COMP mango",
     "yala_like_kind", 5, js(["ngayu", "wawu", "jirray", "mayi", "yala", "mayngku"]), js([]),
     "Patz 4.9.3 ex.606 gives this fruit-like-mango frame.", "0.92"),
    (16, "Some camps are just the same.",
     "Kanbal bayan yala-rrku.",
     "kanbal bayan yala-rrku | some camp COMP-EMPH4",
     "yalarrku_same", 4, js(["kanbal", "bayan", "yala-rrku"]), js([]),
     "Varies Patz 3.9.2 ex.206 Kanbal bubu yala-rrku after exact-duplicate preflight.", "0.82"),
    (17, "This place is just the same.",
     "Bubu yanya yala-rrku.",
     "bubu yanya yala-rrku | place this COMP-EMPH4",
     "yalarrku_same_demonstrative", 4, js(["bubu", "yanya", "yala-rrku"]), js([]),
     "Uses yala-rrku only in the attested 'just the same' comparison sense.", "0.82"),
    (18, "OK, let us go to the water.",
     "Yala, ngana bana-ka dungay.",
     "yala, ngana bana-ka dunga-y | OK/let.us we.INC water-DAT go-NONPAST",
     "yala_interjection_suggestion", 5, js(["yala", "ngana", "bana", "dungay"]), js([]),
     "Patz 4.10: yala as interjection means OK/let's and precedes a suggestion.", "0.86"),

    (19, "I am not hungry.",
     "Ngayu kari dakuy.",
     "ngayu kari dakuy | I NEG hungry",
     "kari_predicate_negation", 5, js(["ngayu", "kari", "dakuy"]), js([]),
     "Patz 4.9.2 ex.598a gives the kari-before-adjective negation frame.", "0.92"),
    (20, "I am hungerless.",
     "Ngayu dakuy-kari.",
     "ngayu dakuy-kari | I hungry-PRIV",
     "kari_privative_predicator", 5, js(["ngayu", "dakuy-kari"]), js([]),
     "Patz 4.9.2 ex.598b contrasts adjective plus -kari privative with pre-adjectival kari.", "0.90"),
    (21, "A ghost is not coming; a man is coming.",
     "Dubu kari kankaday, dingkar kankaday.",
     "dubu kari kankada-y, dingkar kankada-y | ghost NEG come.REDUP-NONPAST man come.REDUP-NONPAST",
     "kari_subject_contrast", 5, js(["dubu", "kari", "kankaday", "dingkar"]), js([]),
     "Patz 4.9.2 ex.596 licenses dubu kari kankaday, dingkar.", "0.88"),
    (22, "The ghost is not coming; it is going.",
     "Dubu kari kankaday, dungay.",
     "dubu kari kankada-y, dunga-y | ghost NEG come.REDUP-NONPAST go-NONPAST",
     "kari_predicate_contrast", 5, js(["dubu", "kari", "kankaday", "dungay"]), js([]),
     "Patz 4.9.2 ex.597 contrasts ghost not coming but going.", "0.88"),
    (23, "I did not break the spear.",
     "Ngayu kari kalka dumbarrin.",
     "ngayu kari kalka dumbarri-ny | I NEG spear break-PAST",
     "kari_object_predicate_negation", 4, js(["ngayu", "kari", "kalka", "dumbarri"]), js([]),
     "Varies Patz ex.593 billycan negation after exact-duplicate preflight.", "0.82"),
    (24, "Not the billycan; the spear broke.",
     "Jungkalu kari dumbarrin, kalka dumbarrin.",
     "jungkalu kari dumbarri-ny, kalka dumbarri-ny | billycan NEG break-PAST spear break-PAST",
     "kari_object_focus_contrast", 5, js(["jungkalu", "kari", "dumbarri", "kalka"]), js(["jungkalu"]),
     "Patz 4.9.2 ex.594 licenses noun-following kari for object contrast.", "0.84"),
    (25, "The child does not know the language.",
     "Karrkay binal-kari kuku-ku.",
     "karrkay binal-kari kuku-ku | child know-PRIV language-DAT",
     "adjectival_particle_privative", 5, js(["karrkay", "binal-kari", "kuku"]), js([]),
     "Patz 4.9.1 ex.584 has binal-kari with kuku-ku.", "0.88"),
    (26, "The old man did not really sleep.",
     "Mukul-dingkar kari-rrku warngku-wunan.",
     "mukul-dingkar kari-rrku warngku-wuna-ny | old-man NEG-EMPH4 sleep-lie-PAST",
     "karirrku_attenuated_negation", 5, js(["mukul-dingkar", "kari-rrku", "warngku-wunay"]), js([]),
     "Patz 3.9.2 ex.208 gives kari-rrku 'not really' before warngku-wuna-ny.", "0.88"),
    (27, "The rain has not come yet.",
     "Kaba kariku kaday.",
     "kaba kari-ku kada-y | rain NEG-EMPH1/not.yet come-NONPAST",
     "kariku_not_yet", 4, js(["kaba", "kariku", "kaday"]), js([]),
     "Patz 3.9.2 says kari-ku can carry temporal 'not yet' force; dictionary has kariku 'not yet'.", "0.80"),

    (28, "The young man disapproves of fighting.",
     "Warru balu kuni-wa-nka.",
     "warru balu kuni-wa-nka | young.man NEG2 hit-RECIP-PURP",
     "balu_disapproval_purposive", 5, js(["warru", "balu", "kuniwan-ka"]), js([]),
     "Patz 4.9.2 ex.600 gives balu with fight-PURP as principled disapproval.", "0.88"),
    (29, "The women disapprove of smoking.",
     "Jalbu-jalbu balu buymbi-nka.",
     "jalbu-jalbu balu buymbi-nka | women NEG2 smoke-PURP",
     "balu_disapproval_purposive", 5, js(["jalbu", "balu", "buymbi"]), js([]),
     "Patz 4.9.2 ex.601 gives balu with buymbi-nka.", "0.88"),
    (30, "Grandmother dislikes half-ripe mangoes on principle.",
     "Babi balu mayngku-ku wawumu-ku.",
     "babi balu mayngku-ku wawumu-ku | grandmother NEG2 mango-DAT half.ripe-DAT",
     "balu_dative_principled_dislike", 5, js(["babi", "balu", "mayngku", "wawumu"]), js([]),
     "Patz 4.9.2 ex.602 gives balu with a dative NP for principled avoidance.", "0.88"),
    (31, "The man could not spear the fish; he gave up.",
     "Dingkar balu dama-nka kuyu.",
     "dingkar balu dama-nka kuyu | man NEG2 spear-PURP fish",
     "balu_inability_give_up", 5, js(["dingkar", "balu", "damal", "kuyu"]), js([]),
     "Modeled on Patz 4.9.2 ex.599, replacing the turtle token with kuyu to avoid the ngawuya spelling trap.", "0.82"),
    (32, "Let the child sleep.",
     "Balu karrkay wuna.",
     "balu karrkay wuna | let/desist child sleep.IMP",
     "balu_imperative_let_be", 4, js(["balu", "karrkay", "wunay"]), js([]),
     "Patz 3.9.2 and 4.9.2 note balu/baluda as 'let it be, desist'.", "0.80"),
    (33, "Leave the meat alone.",
     "Balu minya bawa.",
     "balu minya bawa | let/desist meat leave.IMP",
     "balu_imperative_desist", 4, js(["balu", "minya", "bawal"]), js([]),
     "Uses balu as a desist particle with a simple imperative verb.", "0.78"),
    (34, "I thought you meant to roast the meat, but you are boiling it in water.",
     "Balu yundu minya dinda-nka, kari yundu bana-nga wajul.",
     "balu yundu minya dinda-nka, kari yundu bana-nga waju-l | wrong you meat roast-PURP but you water-LOC cook-NONPAST",
     "balu_wrong_assumption", 5, js(["balu", "yundu", "minya", "dindal", "kari", "bana", "wajul"]), js([]),
     "Patz 5.5.3 ex.658 gives balu for speaker's wrong assumption.", "0.90"),
    (35, "I thought he was coming back.",
     "Balu nyulu kadan-baja.",
     "balu nyulu kada-ny-baja | wrong he come-PAST-again",
     "balu_wrong_assumption", 5, js(["balu", "nyulu", "kaday", "baja"]), js([]),
     "Patz 5.5.3 ex.659 gives Balu nyulu kada-ny-baja.", "0.90"),
    (36, "I thought the dog came.",
     "Balu kaya kadan.",
     "balu kaya kada-ny | wrong dog come-PAST",
     "balu_wrong_assumption", 4, js(["balu", "kaya", "kaday"]), js([]),
     "Extends the wrong-assumption particle to a source-checked simple past clause.", "0.80"),

    (37, "If the rain comes tomorrow, we will stay at the camp.",
     "Kaki kaba kaday wunkuny, ngana bayan-ba bundanday.",
     "kaki kaba kada-y wunkuny, ngana bayan-ba bundanda-y | if rain come-NONPAST tomorrow we.INC camp-LOC stay-NONPAST",
     "kaki_conditional", 4, js(["kaki", "kaba", "kaday", "wunkuny", "ngana", "bayan", "bunday"]), js([]),
     "Dictionary and Patz 3.9.2 list kaki as if/when; row keeps it clause-initial.", "0.80"),
    (38, "If you go, close the house.",
     "Kaki yundu dungay, bayan nanda.",
     "kaki yundu dunga-y, bayan nanda | if you go-NONPAST house close.IMP",
     "kaki_if_when_imperative", 4, js(["kaki", "yundu", "dungay", "bayan", "nandal"]), js([]),
     "Varries the dictionary kaki example while avoiding the English loan 'gate'.", "0.78"),
    (39, "People might talk about you.",
     "Bama kunka balka-wanji yununku.",
     "bama kunka balka-wa-nyji yununku | people in.case/might talk-RECIP-PRECAUT you.DAT",
     "kunka_precautionary_discourse", 5, js(["bama", "kunka", "balkal", "yununku"]), js([]),
     "Dictionary kunka example: Bama kunka balkawanji yununku.", "0.88"),
    (40, "Bring a spear in case a pig comes.",
     "Kalka wundi, nganganjirrka bikibiki kada-nyamunku.",
     "kalka wundi, nganganjirrka bikibiki kada-nyamunku | spear bring.IMP just.in.case pig come-precautionary/dependent",
     "nganganjirrka_precautionary_discourse", 5, js(["kalka", "wundil", "nganganjirrka", "bikibiki", "kaday"]), js([]),
     "Modeled on dictionary nganganjirrka example 'Bring gun, in case a pig comes', with kalka replacing gun.", "0.82"),
    (41, "I speared a pig, but it did not fall down.",
     "Ngayu bikibiki kunin, yamba nyulu kari daran.",
     "ngayu bikibiki kuni-ny, yamba nyulu kari dara-ny | I pig hit/spear-PAST but it NEG fall-PAST",
     "yamba_contrast_state", 5, js(["ngayu", "bikibiki", "kunil", "yamba", "nyulu", "kari", "daral"]), js([]),
     "Dictionary yamba example gives this pig-spearing contrast frame.", "0.88"),
    (42, "The dog is not vicious, but its teeth are bad.",
     "Kaya kuli-ji kari, yamba dirra buyun.",
     "kaya kuli-ji kari, yamba dirra buyun | dog rage-COMIT NEG but teeth bad",
     "yamba_contrast_state", 5, js(["kaya", "kuli", "kari", "yamba", "dirra", "buyun"]), js([]),
     "Modeled on Patz 5.5.3 ex.656 yamba contrast after a negative state.", "0.82"),

    (43, "Yes, I know where the water is.",
     "Yuwu, ngayu binal wanja bana yinyay.",
     "yuwu, ngayu binal wanja bana yinyay | yes I know where water there",
     "interjection_yes_answer", 5, js(["yuwu", "ngayu", "binal", "wanja", "bana", "yinyay"]), js([]),
     "Patz 4.10 lists yuwu 'yes' as an answer interjection.", "0.88"),
    (44, "No, the child did not go.",
     "Kari, karrkay kari dungan.",
     "kari, karrkay kari dunga-ny | no child NEG go-PAST",
     "interjection_no_plus_negation", 5, js(["kari", "karrkay", "dungay"]), js([]),
     "Patz 4.10 lists kari as a 'no' answer; second kari is ordinary clause negation.", "0.88"),
    (45, "Wait for me, I am coming.",
     "Kaku, ngayu kaday.",
     "kaku, ngayu kada-y | wait.for.me I come-NONPAST",
     "interjection_wait_for_me", 5, js(["kaku", "ngayu", "kaday"]), js([]),
     "Patz 4.10 lists kaku as 'just a minute, wait for me'.", "0.88"),
    (46, "Oh yes, I know.",
     "Ngay, ngayu binal.",
     "ngay, ngayu binal | oh.yes I know",
     "interjection_acknowledgement", 5, js(["ngay", "ngayu", "binal"]), js([]),
     "Patz 4.10 lists ngay as 'oh yes, I see' conversational acknowledgement.", "0.86"),
    (47, "Settled then, we will stay at the camp.",
     "Yalada, ngana bayan-ba bundanday.",
     "yalada, ngana bayan-ba bundanda-y | settled.then we.INC camp-LOC stay-NONPAST",
     "interjection_settled_then", 5, js(["yalada", "ngana", "bayan", "bunday"]), js([]),
     "Patz 4.10 lists yalada as 'that's all right, that's settled then'.", "0.88"),
    (48, "OK, let us go.",
     "Yala, ngana dungay.",
     "yala, ngana dunga-y | OK/let.us we.INC go-NONPAST",
     "interjection_ok_lets", 5, js(["yala", "ngana", "dungay"]), js([]),
     "Patz 4.10 lists interjectional yala as OK/let's before a suggestion.", "0.88"),
    (49, "Nonsense, that is not good.",
     "Juyuy, yinya kari ngulkurr.",
     "juyuy, yinya kari ngulkurr | nonsense that NEG good",
     "interjection_nonsense", 5, js(["juyuy", "yinya", "kari", "ngulkurr"]), js([]),
     "Patz 4.10 and dictionary juyuy: 'that's nonsense, take no notice'.", "0.86"),
    (50, "Never mind, we will go again.",
     "Wumba, ngana baja dungay.",
     "wumba, ngana baja dunga-y | never.mind we.INC again go-NONPAST",
     "interjection_never_mind", 5, js(["wumba", "ngana", "baja", "dungay"]), js([]),
     "Patz 4.10 lists wumba as 'never mind'; dictionary has the related no-reason sense.", "0.84"),
]


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 blocked/special token {word!r}")
        for tok in re.sub(r"[.!?,]", " ", kuku.lower()).split():
            if tok.strip("'\"") in DENY_STANDALONE:
                issues.append(f"{label}: denied standalone special interjection {tok!r}")
        if "yala coordination" in english.lower() or " yala-coordination " in low:
            issues.append(f"{label}: yala coordination is excluded by Patz 4.9.3 evidence note")
        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 111 draft: discourse particles, interjections, and answer words", "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 111 source pass reread Patz 4.9.1 adjectival particles, 4.9.2 kari/balu negation, "
            "4.9.3 yala and nguba, 4.10 interjections, and 5.5.3 yamba/balu discourse functions. "
            "Dictionary checks covered nguba, kari, balu, yala, yuwu, kaku, ngay, yalada, juyuy, "
            "wumba, kaki, kunka, nganganjirrka, yamba, and all open-class support words."
        ),
        command="rg/sed over grammar.md and dictionary.yaml; sqlite exact-surface candidate check",
        artifact_path="process_logs/batch-153-particles-interjections-process.md",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Scope is closed-class particle and interjection practice, not free conversational invention. "
            "Excluded yah/Ah attention forms, monosyllabic ba and ma, and walnga because the living errata "
            "already marks exact attention/monosyllabic interjection work as requiring a deliberate lint/lexeme "
            "extension. Excluded yala as a coordination particle because Patz says those examples were not "
            "whole-heartedly accepted."
        ),
        artifact_path="process_logs/batch-153-particles-interjections-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 Kanbal bubu yala-rrku. (batch-127.03) and Ngayu kari "
            "jungkalu dumbarrin. (batch-149.11). Final replacements were Kanbal bayan yala-rrku. and "
            "Ngayu kari kalka dumbarrin.; the final 50-row set was duplicate-clean before banking."
        ),
        command="python3 inline candidate lint/duplicate probe",
        artifact_path="process_logs/batch-153-particles-interjections-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 111 particle/interjection rows under_review after static and duplicate checks.",
        command="python3 batch_scripts/bank_153.py",
        artifact_path="batch_scripts/bank_153.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-10 cover nguba assumptions and alternatives; 11-18 cover yala comparison, yala-rrku, "
            "and yala OK/let's; 19-27 cover kari negation, privative -kari, kari-rrku, and kariku; "
            "28-36 cover balu disapproval, desist/let-be, inability/give-up, and wrong-assumption uses; "
            "37-42 cover kaki, kunka, nganganjirrka, and yamba; 43-50 cover yuwu, kari, kaku, ngay, "
            "yalada, yala, juyuy, and wumba as interjections."
        ),
        artifact_path="batch_scripts/bank_153.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())
