#!/usr/bin/env python3
"""Bank batch-149: camp objects, containers, and everyday implements."""

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

DENY = {
    "table", "gate", "bucket", "cassette", "torch", "brake", "truck", "motorcar", "store",
    "milbayarr", "yinkar", "yinba", "wunbu-kari", "wawu-mumbu", "bikirr", "ngawuya",
}


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


ROWS = [
    (1, "The two women set up camp.",
     "Bula jalbu-bulal-angka bayan ngaran.",
     "bula jalbu-bulal-angka bayan ngara-ny | they.two woman-pair-ERG camp set.up-PAST",
     "camp_setup_source", 5, js(["bula", "jalbu", "bayan", "ngara"]), js([]),
     "Patz ex.234 gives bula jalbu-bulal-angka bayan ngara-ny for setting up camp.", "0.92"),
    (2, "Grandmother sweeps the ground for the camp.",
     "Babi-ngka bubu kidal bayan-ka.",
     "babi-ngka bubu kida-l bayan-ka | grandmother-ERG ground sweep-NONPAST camp-DAT",
     "camp_preparation", 5, js(["babi", "bubu", "kidal", "bayan"]), js([]),
     "Patz camp-preparation examples use bubu kida-l before or for bayan ngara-ny.", "0.88"),
    (3, "I sweep the camp.",
     "Ngayu bayan kidal.",
     "ngayu bayan kida-l | I camp sweep-NONPAST",
     "camp_cleaning", 4, js(["ngayu", "bayan", "kidal"]), js([]),
     "Controlled lexical row: bayan camp/house/shelter plus kida-l sweep/clean.", "0.82"),
    (4, "The young men stayed in the camp.",
     "Warru-warru bayan-ba bundandan.",
     "warru-warru bayan-ba bundanda-ny | young.man-REDUP camp-LOC stay.REDUP-PAST",
     "camp_location", 4, js(["warru", "bayan", "bunday"]), js([]),
     "Bayan-ba is the regular camp/house locative frame; bundanda-ny is a stable stay/sit predicate.", "0.82"),
    (5, "The fire is there in the camp.",
     "Baya bayan-ba yinyay.",
     "baya bayan-ba yinyay | fire camp-LOC there",
     "camp_fire_location", 4, js(["baya", "bayan", "yinyay"]), js([]),
     "Concrete location row: baya means fire/flame/firewood; yinyay is the attested there demonstrative.", "0.82"),
    (6, "He collected firewood.",
     "Nyulu baya mujan.",
     "nyulu baya muja-ny | he firewood collect-PAST",
     "firewood_collect", 5, js(["nyulu", "baya", "mujal"]), js([]),
     "Dictionary entry for baya includes example nyulu baya mujan 'he collected firewood'.", "0.92"),
    (7, "I light the fire so you get warm.",
     "Ngayu baya wajul, yundu wumbul-manka.",
     "ngayu baya waju-l, yundu wumbul-ma-nka | I fire light/cook-NONPAST, you warm-INCH-PURP",
     "fire_purpose", 5, js(["ngayu", "baya", "wajul", "yundu", "wumbul-mal"]), js([]),
     "Patz purposive fire example has baya waju-l so the hearer gets warm.", "0.88"),
    (8, "Grandmother lit the fire at the camp.",
     "Babi-ngka baya wajun bayan-ba.",
     "babi-ngka baya waju-ny bayan-ba | grandmother-ERG fire light/cook-PAST camp-LOC",
     "fire_at_camp", 5, js(["babi", "baya", "wajul", "bayan"]), js([]),
     "Replacement after duplicate preflight: keeps waju-ny past 'wajun' and concrete bayan-ba location.", "0.86"),
    (9, "The charcoal is there in the fire.",
     "Ngunjil baya-nga yinyay.",
     "ngunjil baya-nga yinyay | charcoal/hot.coals fire-LOC there",
     "fire_coals_location", 4, js(["ngunjil", "baya", "yinyay"]), js([]),
     "Dictionary gives ngunjil as charcoal, hot coals, or hot ashes; not a substitute for baya itself.", "0.84"),
    (10, "Grandmother washed the billycan at the creek.",
     "Babi-ngka jungkalu julurrin wawubaja-nga.",
     "babi-ngka jungkalu julurri-ny wawubaja-nga | grandmother-ERG billycan wash-PAST creek-LOC",
     "container_washing", 5, js(["babi", "jungkalu", "julurril", "wawubaja"]), js(["jungkalu"]),
     "Jungkalu is the dictionary billycan entry; julurril wash and wawubaja creek are stable.", "0.84"),
    (11, "I did not break the billycan.",
     "Ngayu kari jungkalu dumbarrin.",
     "ngayu kari jungkalu dumbarri-ny | I NEG billycan break-PAST",
     "container_neg_break", 5, js(["ngayu", "kari", "jungkalu", "dumbarril"]), js(["jungkalu"]),
     "Patz negation examples use jungkalu plus dumbarri-ny; word order varied to avoid exact duplicate.", "0.88"),
    (12, "Put the fish on the plate.",
     "Kuyu bulin-ba yijarri-ka!",
     "kuyu bulin-ba yijarri-ka | fish plate-LOC put-IMP",
     "plate_put_imperative", 5, js(["kuyu", "bulin", "yijarril"]), js(["bulin"]),
     "Patz ex.200 has bulin-ba yijarri-ka with an object on a plate; object varied to kuyu.", "0.86"),
    (13, "The child washed the plate in the water.",
     "Karrkay-ngku bulin julurrin bana-nga.",
     "karrkay-ngku bulin julurri-ny bana-nga | child-ERG plate wash-PAST water-LOC",
     "plate_washing", 4, js(["karrkay", "bulin", "julurril", "bana"]), js(["bulin"]),
     "Concrete container/utensil row using source-backed bulin plate and julurril wash.", "0.80"),
    (14, "Water is there in the billycan.",
     "Bana jungkalu-ngu yinyay.",
     "bana jungkalu-ngu yinyay | water billycan-LOC there",
     "container_locative", 4, js(["bana", "jungkalu", "yinyay"]), js(["jungkalu"]),
     "Uses correct u-final locative -ngu on jungkalu and keeps the proposition concrete.", "0.82"),
    (15, "Pour water into the billycan.",
     "Bana nyangarri-ka jungkalu-ngu!",
     "bana nyangarri-ka jungkalu-ngu | water pour-IMP billycan-LOC",
     "container_pour_imperative", 5, js(["bana", "nyangarril", "jungkalu"]), js(["jungkalu"]),
     "Nyangarril pour/spill plus jungkalu-ngu follows attested pour-into-container pattern.", "0.84"),

    (16, "What did you put in the bag?",
     "Yundu wanyu mumban balji-nga?",
     "yundu wanyu mumba-ny balji-nga | you what put.inside-PAST bag-LOC",
     "bag_question_source", 5, js(["yundu", "wanyu", "mumbal", "balji"]), js([]),
     "Patz ex.579 gives the wanyu mumba-ny balji-nga question frame.", "0.92"),
    (17, "I put food in the bag.",
     "Ngayu mayi mumban balji-nga.",
     "ngayu mayi mumba-ny balji-nga | I food put.inside-PAST bag-LOC",
     "bag_put_inside", 5, js(["ngayu", "mayi", "mumbal", "balji"]), js([]),
     "Direct declarative variant of the Patz bag-location mumbal frame.", "0.86"),
    (18, "My bag broke. I have to make a new bag.",
     "Ngayku balji kadaban. Ngayu balji jirakal balka-nka.",
     "ngayku balji kadaba-ny. ngayu balji jirakal balka-nka | my bag break-PAST. I bag new make-PURP",
     "bag_repair_source", 5, js(["ngayku", "balji", "kadabal", "ngayu", "jirakal", "balkal"]), js([]),
     "Patz ex.402 has the broken-bag plus new-bag-making frame.", "0.92"),
    (19, "The woman wove the bag from loya cane.",
     "Jalbu-ngku balji wukurrin yibuy-muny.",
     "jalbu-ngku balji wukurri-ny yibuy-muny | woman-ERG bag weave-PAST loya.cane-ABL",
     "bag_weave_material", 5, js(["jalbu", "balji", "wukurril", "yibuy"]), js([]),
     "Patz ex.283 licenses balji wukurri-ny from plant material; yibuy is a sourced loya-cane material.", "0.86"),
    (20, "The woman wove the dillybag from loya cane.",
     "Jalbu-ngku ngunyin wukurrin yibuy-muny.",
     "jalbu-ngku ngunyin wukurri-ny yibuy-muny | woman-ERG dillybag weave-PAST loya.cane-ABL",
     "dillybag_weave_material", 5, js(["jalbu", "ngunyin", "wukurril", "yibuy"]), js([]),
     "Ngunyin is a dillybag/bark-strip entry; wukurril weave remains source-backed.", "0.84"),
    (21, "Grandmother made a new dillybag.",
     "Babi-ngka ngunyin jirakal bungan.",
     "babi-ngka ngunyin jirakal bunga-ny | grandmother-ERG dillybag new make-PAST",
     "dillybag_make", 4, js(["babi", "ngunyin", "jirakal", "bungal"]), js([]),
     "Concrete make row; jirakal new is attested and ngunyin is kept as ordinary dillybag, not placenta/web sense.", "0.80"),
    (22, "The older sister made a sack for mother to carry.",
     "Babarr-angka kurmun balkan, ngamu-ngku jarrakulba-nka.",
     "babarr-angka kurmun balka-ny, ngamu-ngku jarrakulba-nka | older.sister-ERG sack make-PAST, mother-ERG carry-PURP",
     "sack_carry_purpose_source", 5, js(["babarr", "kurmun", "balkal", "ngamu", "jarrakulbal"]), js([]),
     "Patz ex.413 gives babarr-angka kurmun balka-l/balka-ny with ngamu-ngku jarrakulba-nka.", "0.90"),
    (23, "I put food in the sack.",
     "Ngayu mayi mumban kurmun-ba.",
     "ngayu mayi mumba-ny kurmun-ba | I food put.inside-PAST sack-LOC",
     "sack_put_inside", 4, js(["ngayu", "mayi", "mumbal", "kurmun"]), js([]),
     "Container locative extension from the balji mumbal frame to sourced kurmun sack/bag.", "0.78"),
    (24, "The woman tied the sack with rope.",
     "Jalbu-ngku kurmun karra-bu kajan.",
     "jalbu-ngku kurmun karra-bu kaja-ny | woman-ERG sack rope-INST tie-PAST",
     "rope_tie_instrument", 5, js(["jalbu", "kurmun", "karra", "kajal"]), js([]),
     "Instrumental implement frame: karra-bu marks rope as tool, not agent.", "0.84"),
    (25, "The man tied the rope to the bag.",
     "Dingkar-angka karra balji-nga kuna-bungan.",
     "dingkar-angka karra balji-nga kuna-bunga-ny | man-ERG rope bag-LOC join/tie-PAST",
     "rope_join_to_bag", 4, js(["dingkar", "karra", "balji", "kuna-bungal"]), js([]),
     "Kuna-bungal is the sourced join/tie-to verb; balji-nga is the target location.", "0.80"),
    (26, "The rope lay in the bag.",
     "Karra balji-nga wunan.",
     "karra balji-nga wuna-ny | rope bag-LOC lie-PAST",
     "rope_in_bag", 4, js(["karra", "balji", "wunay"]), js([]),
     "Concrete locative row linking karra rope/cord to balji-nga container location.", "0.82"),
    (27, "Grandmother made a bag from loya cane.",
     "Babi-ngka balji yibuy-muny bungan.",
     "babi-ngka balji yibuy-muny bunga-ny | grandmother-ERG bag loya.cane-ABL make-PAST",
     "bag_make_material", 4, js(["babi", "balji", "yibuy", "bungal"]), js([]),
     "Keeps material-source morphology explicit with yibuy-muny and a simple make verb.", "0.80"),
    (28, "I put leaves in the sack.",
     "Ngayu birra mumban kurmun-ba.",
     "ngayu birra mumba-ny kurmun-ba | I leaves put.inside-PAST sack-LOC",
     "sack_leaf_contents", 4, js(["ngayu", "birra", "mumbal", "kurmun"]), js([]),
     "Birra leaf plus kurmun-ba keeps the container vocabulary concrete and source-bounded.", "0.78"),
    (29, "I poured water into the bark container.",
     "Ngayu bana nyangarrin dubal-ba.",
     "ngayu bana nyangarri-ny dubal-ba | I water pour-PAST bark.container-LOC",
     "bark_container_pour", 5, js(["ngayu", "bana", "nyangarril", "dubal"]), js([]),
     "Patz text lines use bana nyangarri-ny dubal-ba, with dubal as bark/coolamon container.", "0.88"),
    (30, "Grandmother chopped bark for the coolamon.",
     "Babi-ngka dubal nyandan.",
     "babi-ngka dubal nyanda-ny | grandmother-ERG bark/coolamon chop-PAST",
     "bark_chop", 5, js(["babi", "dubal", "nyandal"]), js([]),
     "Patz text line 61 has dubal nyanda-nya; this keeps dubal as bark/coolamon material.", "0.86"),
    (31, "The bark container is there in the camp.",
     "Dubal bayan-ba yinyay.",
     "dubal bayan-ba yinyay | bark.container camp-LOC there",
     "bark_container_location", 4, js(["dubal", "bayan", "yinyay"]), js([]),
     "Risk-controlled dubal row: a concrete bark/coolamon container, not a generic bowl or funerary object.", "0.78"),

    (32, "That spear is new.",
     "Yinya kalka jirakal.",
     "yinya kalka jirakal | that spear new",
     "spear_quality", 5, js(["yinya", "kalka", "jirakal"]), js([]),
     "Patz noun-phrase section gives kalka (jirakal) as a new spear example.", "0.88"),
    (33, "His spear is blunt.",
     "Nyungu kalka dudu.",
     "nyungu kalka dudu | his spear blunt",
     "spear_quality", 5, js(["nyungu", "kalka", "dudu"]), js([]),
     "Patz ex.300 has nyungu kalka dudu as the blunt-spear frame.", "0.90"),
    (34, "I have to make a spear.",
     "Ngayu kalka balka-nka.",
     "ngayu kalka balka-nka | I spear make-PURP",
     "spear_make_purpose", 4, js(["ngayu", "kalka", "balkal"]), js([]),
     "Replacement after duplicate preflight; keeps Patz wawu/balka-nka spear-making source frame simple.", "0.82"),
    (35, "The young man got the spear thrower.",
     "Warru-ngku balur manin.",
     "warru-ngku balur mani-ny | young.man-ERG spear.thrower get-PAST",
     "spear_thrower_get", 5, js(["warru", "balur", "manil"]), js([]),
     "Balur is the ordinary dictionary spear thrower/woomera entry; Milbayarr was deliberately avoided.", "0.84"),
    (36, "The young man speared the turtle with the spear thrower.",
     "Warru-ngku ngawiya balur-bu daman.",
     "warru-ngku ngawiya balur-bu dama-ny | young.man-ERG sea.turtle spear.thrower-INST spear-PAST",
     "spear_thrower_instrument", 5, js(["warru", "ngawiya", "balur", "damal"]), js([]),
     "Instrumental implement frame from Patz spear-with-implement examples; spelling guard uses ngawiya, not ngawuya.", "0.84"),
    (37, "The young man made a spear from hard wood.",
     "Warru-ngku kalka bungan juku junkurr-muny.",
     "warru-ngku kalka bunga-ny juku junkurr-muny | young.man-ERG spear make-PAST wood hard-ABL",
     "spear_make_material", 4, js(["warru", "kalka", "bungal", "juku", "junkurr"]), js([]),
     "Material-source row using juku wood/tree plus junkurr strong/hard; no invented timber term.", "0.78"),
    (38, "Grandmother cut the meat with a knife.",
     "Babi-ngka minya naybu-bu yakan.",
     "babi-ngka minya naybu-bu yaka-ny | grandmother-ERG meat knife-INST cut-PAST",
     "knife_instrument", 5, js(["babi", "minya", "naybu", "yakal"]), js(["naybu"]),
     "Patz ex.247 has minya yaka-l naybu-bu; naybu is an attested English loan and kept sparse.", "0.88"),
    (39, "My knife is blunt.",
     "Ngayku naybu dudu.",
     "ngayku naybu dudu | my knife blunt",
     "knife_quality", 4, js(["ngayku", "naybu", "dudu"]), js(["naybu"]),
     "Concrete property row for the attested knife loan; avoids adding modern household vocabulary.", "0.78"),
    (40, "The man lit the fire with a firestick.",
     "Dingkar-angka jimal-bu baya wajun.",
     "dingkar-angka jimal-bu baya waju-ny | man-ERG firestick-INST fire light/cook-PAST",
     "firestick_instrument", 5, js(["dingkar", "jimal", "baya", "wajul"]), js([]),
     "Jimal is fire stick; -bu marks the implement as instrumental.", "0.84"),
    (41, "The firestick is there in the fire.",
     "Jimal baya-nga yinyay.",
     "jimal baya-nga yinyay | firestick fire-LOC there",
     "firestick_location", 4, js(["jimal", "baya", "yinyay"]), js([]),
     "Concrete location row tying jimal to baya-nga without treating it as agentive.", "0.80"),
    (42, "The stick lay on the ground.",
     "Jaba bubu-ngu wunan.",
     "jaba bubu-ngu wuna-ny | stick ground-LOC lie-PAST",
     "stick_location", 4, js(["jaba", "bubu", "wunay"]), js([]),
     "Jaba covers stick/sapling/post/walking stick; bubu-ngu is the correct u-final ground locative.", "0.80"),
    (43, "I got a stick.",
     "Ngayu jaba manin.",
     "ngayu jaba mani-ny | I stick get-PAST",
     "stick_get", 4, js(["ngayu", "jaba", "manil"]), js([]),
     "Very low-risk jaba object row after avoiding unsupported digging-stick elaboration.", "0.82"),
    (44, "Grandmother grabbed the food with tongs.",
     "Babi-ngka bangkal-bu mayi karrban.",
     "babi-ngka bangkal-bu mayi karrba-ny | grandmother-ERG tongs-INST food grab-PAST",
     "tongs_instrument", 5, js(["babi", "bangkal", "mayi", "karrbal"]), js([]),
     "Bangkal is used in the noun sense 'tongs', not the homophonous gather verb.", "0.84"),
    (45, "The tongs are there in the earth oven.",
     "Bangkal kurrma-nga yinyay.",
     "bangkal kurrma-nga yinyay | tongs earth.oven-LOC there",
     "tongs_earth_oven_location", 5, js(["bangkal", "kurrma", "yinyay"]), js([]),
     "Dictionary defines bangkal as tongs made from voya vine used in a kurrma earth oven.", "0.90"),

    (46, "The dugout canoe went on the sea.",
     "Marrakan jalun-bu dungan.",
     "marrakan jalun-bu dunga-ny | dugout.canoe sea-LOC go-PAST",
     "canoe_sea_motion", 5, js(["marrakan", "jalun", "dungay"]), js([]),
     "Marrakan is the dugout canoe made from red cedar; jalun-bu sea location is source-backed.", "0.84"),
    (47, "The young men sat in the dugout canoe.",
     "Warru-warru marrakan-ba bundandan.",
     "warru-warru marrakan-ba bundanda-ny | young.man-REDUP dugout.canoe-LOC sit.REDUP-PAST",
     "canoe_location", 4, js(["warru", "marrakan", "bunday"]), js([]),
     "Marrakan-ba uses consonant-final locative -ba; bundandan is stable sit/stay past.", "0.80"),
    (48, "The men pulled the dugout canoe on the sand.",
     "Dingkar-dingkar-angka marrakan jukar-a kulban.",
     "dingkar-dingkar-angka marrakan jukar-a kulba-ny | man-REDUP-ERG dugout.canoe sand-LOC pull-PAST",
     "canoe_pull_sand", 4, js(["dingkar", "marrakan", "jukar", "kulbal"]), js([]),
     "Jukar-a follows the single-r locative pattern; kulbal covers pull/lift.", "0.80"),
    (49, "Mother washed the clothing at the creek.",
     "Ngamu-ngku kambi julurrin wawubaja-nga.",
     "ngamu-ngku kambi julurri-ny wawubaja-nga | mother-ERG clothing wash-PAST creek-LOC",
     "clothing_washing", 4, js(["ngamu", "kambi", "julurril", "wawubaja"]), js([]),
     "Kambi is dictionary clothing/flying-fox polysemy; here the clothing sense is selected by wash.", "0.80"),
    (50, "The clothes lay in the camp.",
     "Kambi bayan-ba wunan.",
     "kambi bayan-ba wuna-ny | clothing camp-LOC lie-PAST",
     "clothing_camp_location", 4, js(["kambi", "bayan", "wunay"]), js([]),
     "Concrete final row: kambi clothing in bayan-ba camp, with ordinary rest predicate.", "0.80"),
]


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 modern/euphemistic/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 107 draft: camp objects, containers, and everyday implements", "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 107 source pass reread Rules 83, 91, 95, 96, 104, 105, and 106 plus the errata. "
            "Grammar checks covered bayan ngara-ny camp setup, balji/ngunyin weaving, kurmun carry-purpose, "
            "baya/waju-l fire rows, jungkalu negation, bulin/yijarril plate rows, dubal bark/coolamon text lines, "
            "instrumental implement examples, and canoe/sea/sand locatives. Dictionary checks covered the concrete "
            "lexemes used in all 50 rows."
        ),
        command="rg/sed over grammar.md and dictionary.yaml; sqlite exact-duplicate preflight",
        artifact_path="process_logs/batch-149-camp-objects-process.md",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Scope kept to concrete source-backed camp objects and implements. Avoided modern household spread "
            "such as table/gate/bucket/cassette/torch/brake/truck/store, avoided Milbayarr because of mixed "
            "place/euphemism/woomera ambiguity, avoided yinkar because it is not an ordinary bag, and kept naybu, "
            "bulin, and jungkalu sparse and explicitly logged as loan or post-contact terms."
        ),
        artifact_path="process_logs/batch-149-camp-objects-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 Baya waju-l-waju, kiway "
            "kada-nyji. because it already exists in batch-125.07 and rejected Nyulu wawu kalka balka-nka. because "
            "it already exists in batch-140.30. Replacements were Babi-ngka baya wajun bayan-ba. and Ngayu kalka "
            "balka-nka.; the final 50-row set was duplicate-clean before banking."
        ),
        command="python3 sqlite exact-surface candidate check",
        artifact_path="process_logs/batch-149-camp-objects-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 107 camp-object, container, and implement rows under_review after static and duplicate checks.",
        command="python3 batch_scripts/bank_149.py",
        artifact_path="batch_scripts/bank_149.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-15 cover camp/fire/cooking containers; 16-31 cover bags, sacks, rope, and bark containers; "
            "32-45 cover spears, throwers, knives, firesticks, sticks, tongs, and earth ovens; 46-50 cover dugout "
            "canoe and clothing location/washing rows."
        ),
        artifact_path="batch_scripts/bank_149.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())
