#!/usr/bin/env python3
"""Bank batch-151: fine-grained plant, tree, fibre, seed, bark, and material lexicon."""

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

DENY = {
    "barka", "wankarr", "garden", "jinajina", "yalanji-jinajina", "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, "Many leaves are there on the tree.",
     "Birra wubul juku-ngu yinyay.",
     "birra wubul juku-ngu yinyay | leaves many tree-LOC there",
     "plant_part_locative", 5, js(["birra", "wubul", "juku", "yinyay"]), js([]),
     "Patz ex.524 licenses birra with juku-ngu for leaves on a tree; birra kept in leaf, not lung, sense.", "0.86"),
    (2, "The leaves from the tree there are big.",
     "Birra yinyay juku-muny yalbay.",
     "birra yinyay juku-muny yalbay | leaves there tree-ABL big",
     "plant_part_ablative", 5, js(["birra", "yinyay", "juku", "yalbay"]), js([]),
     "Patz ex.524 contrasts juku-ngu leaves on a tree with juku-muny leaves from a tree.", "0.86"),
    (3, "This fruit on the tree is good.",
     "Yanyu mayi juku-ngu ngulkurr.",
     "yanyu mayi juku-ngu ngulkurr | this fruit tree-LOC good",
     "fruit_tree_locative", 5, js(["yanyu", "mayi", "juku", "ngulkurr"]), js([]),
     "Variant of Patz ex.525 fruit-on/from-tree frame; exact batch-133 surfaces were avoided.", "0.82"),
    (4, "The shell on the zamia nut is very hard.",
     "Dumbul marra-nga dandi-bajaku.",
     "dumbul marra-nga dandi-bajaku | shell/hard.covering zamia.nut-LOC hard-very",
     "plant_covering_locative", 5, js(["dumbul", "marra", "dandi"]), js([]),
     "Patz ex.526 has dumbul marra-nga dandi; batch varies only the degree particle.", "0.88"),
    (5, "I saw the tree with leaves.",
     "Ngayu juku birra-jirr nyajin.",
     "ngayu juku birra-jirr nyaji-ny | I tree leaf-COMIT see-PAST",
     "tree_leaf_comitative", 4, js(["ngayu", "juku", "birra", "nyajil"]), js([]),
     "Patz ex.544 uses juku-ku birra-jirr-ka; row keeps the ABS comitative attribute simple.", "0.80"),
    (6, "I saw the branches on the tree.",
     "Ngayu dakil-dakil juku-ngu nyajin.",
     "ngayu dakil-dakil juku-ngu nyaji-ny | I branches tree-LOC see-PAST",
     "tree_branch_locative", 4, js(["ngayu", "dakil-dakil", "juku", "nyajil"]), js([]),
     "Dictionary gives dakil-dakil as branches of a tree; juku-ngu follows the plant-part locative frame.", "0.80"),
    (7, "The canopy of the tree is big.",
     "Wakumba juku-ngu yalbay.",
     "wakumba juku-ngu yalbay | canopy tree-LOC big",
     "tree_canopy_locative", 4, js(["wakumba", "juku", "yalbay"]), js([]),
     "Wakumba is the spreading branches/canopy of a tree; juku-ngu marks the host tree.", "0.80"),
    (8, "The base of the tree is hard.",
     "Dingal juku-ngu dandi.",
     "dingal juku-ngu dandi | base tree-LOC hard",
     "tree_base_locative", 4, js(["dingal", "juku", "dandi"]), js([]),
     "Dingal is base/end, including base of a tree; dandi is hard/tough.", "0.80"),
    (9, "The tree stump is there on the ground.",
     "Jakalkulu bubu-ngu yinyay.",
     "jakalkulu bubu-ngu yinyay | tree.stump ground-LOC there",
     "tree_stump_location", 4, js(["jakalkulu", "bubu", "yinyay"]), js([]),
     "Jakalkulu is tree stump; bubu-ngu is the controlled ground locative.", "0.80"),
    (10, "The log lay on the ground.",
     "Walmba bubu-ngu wunan.",
     "walmba bubu-ngu wuna-ny | log ground-LOC lie-PAST",
     "log_ground_location", 4, js(["walmba", "bubu", "wunay"]), js([]),
     "Walmba is log; wuna-ny locative rest frame is already controlled.", "0.82"),
    (11, "The hollow trunk is big.",
     "Dimbirr yalbay.",
     "dimbirr yalbay | hollow.trunk big",
     "hollow_trunk_quality", 4, js(["dimbirr", "yalbay"]), js([]),
     "Dimbirr is a hollow trunk of a tree; kept as a nominal quality row.", "0.80"),
    (12, "The broad leaf lay in the bag.",
     "Bindi balji-nga wunan.",
     "bindi balji-nga wuna-ny | broad.leaf bag-LOC lie-PAST",
     "leaf_container_location", 4, js(["bindi", "balji", "wunay"]), js([]),
     "Bindi is any broad leaf; balji-nga is the previously controlled bag locative.", "0.78"),

    (13, "The two found Leichhardt fruit there.",
     "Bula mayi kabal yinyay warrmba-bungan.",
     "bula mayi kabal yinyay warrmba-bunga-ny | they.two fruit Leichhardt.tree there find-CAUS-PAST",
     "leichhardt_fruit_find", 5, js(["bula", "mayi", "kabal", "yinyay", "warrmba-bungal"]), js([]),
     "Text 51 lines 6-7 have mayi kabal warrmba-bunga-ny; yinyay avoids the accepted exact source surface.", "0.86"),
    (14, "I saw Leichhardt fruit.",
     "Ngayu kabal mayi nyajin.",
     "ngayu kabal mayi nyaji-ny | I Leichhardt.tree fruit see-PAST",
     "leichhardt_fruit_see", 4, js(["ngayu", "kabal", "mayi", "nyajil"]), js([]),
     "Kabal entry covers Leichhardt tree and fruit; mayi selects fruit/vegetable food.", "0.82"),
    (15, "We will climb the tree there.",
     "Ngali yinyay juku-ngu dakal.",
     "ngali yinyay juku-ngu daka-l | we.two.INC there tree-LOC climb-NONPAST",
     "tree_climb_source_variant", 5, js(["ngali", "yinyay", "juku", "dakal"]), js([]),
     "Text 51 line 11 gives ngali juku-ngu daka-l; yinyay avoids the prior exact accepted row.", "0.84"),
    (16, "The two got fruit from the tree.",
     "Bula juku-muny mayi manin.",
     "bula juku-muny mayi mani-ny | they.two tree-ABL fruit get-PAST",
     "fruit_from_tree_get", 5, js(["bula", "juku", "mayi", "manil"]), js([]),
     "Text 51 line 13 has juku-muny mani-l-mani-ny mayi nuka-l-nuka-ny.", "0.84"),
    (17, "The two ate Leichhardt fruit.",
     "Bula kabal mayi nukan.",
     "bula kabal mayi nuka-ny | they.two Leichhardt.tree fruit eat-PAST",
     "leichhardt_fruit_eat", 4, js(["bula", "kabal", "mayi", "nukal"]), js([]),
     "Kabal fruit is source-attested as eaten in Text 51; nuka-ny is the stable eat past.", "0.82"),
    (18, "Grandmother roasted the candlenut.",
     "Babi-ngka bilar dindan.",
     "babi-ngka bilar dinda-ny | grandmother-ERG candlenut roast-PAST",
     "candlenut_roast", 5, js(["babi", "bilar", "dindal"]), js([]),
     "Bilar entry says candlenuts are edible after roasting; dinda-l is roast on coals.", "0.82"),
    (19, "I saw the fig-like fruit.",
     "Ngayu binainyji nyajin.",
     "ngayu binainyji nyaji-ny | I fig.like.fruit see-PAST",
     "fig_like_fruit_see", 4, js(["ngayu", "binainyji", "nyajil"]), js([]),
     "Binainyji is grammar-attested as fig-like fruit; no processing claim is added.", "0.80"),
    (20, "The Banday fruit lay there.",
     "Banday mayi yinyay wunan.",
     "banday mayi yinyay wuna-ny | Banday.tree fruit there lie-PAST",
     "banday_fruit_location", 4, js(["banday", "mayi", "yinyay", "wunay"]), js([]),
     "Banday entry covers a tree and cherry-like fruit; mayi selects the fruit sense.", "0.78"),
    (21, "Many junjun fruits are there.",
     "Junjun mayi wubul yinyay.",
     "junjun mayi wubul yinyay | junjun.wild.fruit fruit many there",
     "junjun_fruit_quantity", 4, js(["junjun", "mayi", "wubul", "yinyay"]), js([]),
     "Junjun is a lexicalized solid reduplicated wild-fruit headword; mayi keeps the fruit sense explicit.", "0.80"),
    (22, "I got the yam.",
     "Ngayu bambayal manin.",
     "ngayu bambayal mani-ny | I yam get-PAST",
     "yam_get", 4, js(["ngayu", "bambayal", "manil"]), js([]),
     "Bambayal is a long thin yam; no preparation claim is added.", "0.78"),
    (23, "Grandmother roasted the wild yam.",
     "Babi-ngka banjay dindan.",
     "babi-ngka banjay dinda-ny | grandmother-ERG wild.yam roast-PAST",
     "wild_yam_roast", 5, js(["babi", "banjay", "dindal"]), js([]),
     "Banjay entry says the small round wild yams are roasted in the fire.", "0.84"),
    (24, "Grandmother boiled the hairy yam in water.",
     "Babi-ngka wukay bana-nga wajun.",
     "babi-ngka wukay bana-nga waju-ny | grandmother-ERG hairy.yam water-LOC cook/boil-PAST",
     "hairy_yam_boiling", 4, js(["babi", "wukay", "bana", "wajul"]), js([]),
     "Wukay requires preparation before eating; Patz ex.658 licenses bana-nga waju-l for boiling in water.", "0.76"),
    (25, "Grandmother boiled the medicinal root in water.",
     "Babi-ngka bangkan bana-nga wajun.",
     "babi-ngka bangkan bana-nga waju-ny | grandmother-ERG medicinal.root water-LOC cook/boil-PAST",
     "medicinal_root_boiling", 4, js(["babi", "bangkan", "bana", "wajul"]), js([]),
     "Bangkan entry says the roots are boiled and the water used to wash sores; row states only the boiling.", "0.78"),
    (26, "I planted the seeds in the ground.",
     "Ngayu diburr nandan bubu-ngu.",
     "ngayu diburr nanda-ny bubu-ngu | I seed bury/plant-PAST ground-LOC",
     "seed_planting_source", 5, js(["ngayu", "diburr", "nandal", "bubu"]), js([]),
     "Diburr usage gives Ngayu diburr nandan gardenba for planted seeds; bubu-ngu replaces the loan garden.", "0.82"),
    (27, "I got seeds.",
     "Ngayu diburr manin.",
     "ngayu diburr mani-ny | I seed get-PAST",
     "seed_get", 4, js(["ngayu", "diburr", "manil"]), js([]),
     "Diburr is seed/egg/bullet/tablet; English and plant batch context select the seed sense.", "0.78"),
    (28, "Many black and red seeds are there.",
     "Kubirr-kubirr diburr wubul yinyay.",
     "kubirr-kubirr diburr wubul yinyay | black.red.seeds seed many there",
     "coloured_seed_quantity", 4, js(["kubirr-kubirr", "diburr", "wubul", "yinyay"]), js([]),
     "Kubirr-kubirr entry names black and red seeds; wankarr was not used because it is not a clean headword.", "0.78"),

    (29, "The woman wove the bag from dillybag grass.",
     "Jalbu-ngku balji wukurrin bilanji-muny.",
     "jalbu-ngku balji wukurri-ny bilanji-muny | woman-ERG bag weave-PAST dillybag.grass-ABL",
     "bag_weave_grass_material", 5, js(["jalbu", "balji", "wukurril", "bilanji"]), js([]),
     "Bilanji is grass used to make dilly bags; Patz ex.283 licenses material-source weaving rows.", "0.82"),
    (30, "The woman wove the bag from coarse grass.",
     "Jalbu-ngku balji wukurrin mawu-muny.",
     "jalbu-ngku balji wukurri-ny mawu-muny | woman-ERG bag weave-PAST coarse.grass-ABL",
     "bag_weave_coarse_grass_material", 5, js(["jalbu", "balji", "wukurril", "mawu"]), js([]),
     "Mawu is coarse grass used in making dilly bags; -muny marks material source.", "0.82"),
    (31, "The woman wove the bag from the black-palm upper fibre.",
     "Jalbu-ngku balji wukurrin kakan-muny.",
     "jalbu-ngku balji wukurri-ny kakan-muny | woman-ERG bag weave-PAST black.palm.upper.part-ABL",
     "bag_weave_black_palm_material", 4, js(["jalbu", "balji", "wukurril", "kakan"]), js([]),
     "Kakan prose says the green upper black-palm part is used to make dilly bags; gloss 'mangrove' is avoided.", "0.78"),
    (32, "Grandmother got loya vine leaves for the shelter.",
     "Babi-ngka bajarr manin bayan-ka.",
     "babi-ngka bajarr mani-ny bayan-ka | grandmother-ERG loya.vine.leaves get-PAST shelter-DAT",
     "shelter_leaf_material_get", 5, js(["babi", "bajarr", "manil", "bayan"]), js([]),
     "Bajarr is loya vine fern leaves used in making mia-mia; bayan-ka keeps the shelter purpose broad.", "0.82"),
    (33, "The paperbark lay there in the earth oven.",
     "Buyku kurrma-nga yinyay bundandan.",
     "buyku kurrma-nga yinyay bundanda-ny | paperbark earth.oven-LOC there sit.REDUP-PAST",
     "paperbark_earth_oven_source", 5, js(["buyku", "kurrma", "yinyay", "bunday"]), js([]),
     "Text 51 line 66 has buyku bundanda-nya covering the meat in the ground oven.", "0.84"),
    (34, "The woman chopped bark.",
     "Jalbu-ngku dubal nyandan.",
     "jalbu-ngku dubal nyanda-ny | woman-ERG bark/coolamon chop-PAST",
     "bark_chop_source_variant", 5, js(["jalbu", "dubal", "nyandal"]), js([]),
     "Text 51 line 61 has dubal nyanda-nya; A noun varied to avoid earlier exact surfaces.", "0.84"),
    (35, "Grandmother poured water into the bark container.",
     "Babi-ngka bana nyangarrin dubal-ba.",
     "babi-ngka bana nyangarri-ny dubal-ba | grandmother-ERG water pour-PAST bark.container-LOC",
     "bark_container_pour", 5, js(["babi", "bana", "nyangarril", "dubal"]), js([]),
     "Text 51 line 63 and Batch 149 control bana nyangarri-ny dubal-ba; A noun varied.", "0.84"),
    (36, "Water is there in the bark container.",
     "Bana dubal-ba yinyay.",
     "bana dubal-ba yinyay | water bark.container-LOC there",
     "bark_container_location", 4, js(["bana", "dubal", "yinyay"]), js([]),
     "Dubal is bark/coolamon/bark container; this row keeps it container-locative, not a generic bowl.", "0.78"),
    (37, "The man made rope from kuralbi bark.",
     "Dingkar-angka karra balkan kuralbi-muny.",
     "dingkar-angka karra balka-ny kuralbi-muny | man-ERG rope make-PAST kuralbi-ABL",
     "rope_tree_bark_material", 4, js(["dingkar", "karra", "balkal", "kuralbi"]), js([]),
     "Kuralbi prose says bark from the tree was twisted into strong rope; row avoids adding turtle-rope detail.", "0.78"),
    (38, "The young man got tree bark used as vine.",
     "Warru-ngku warrur manin.",
     "warru-ngku warrur mani-ny | young.man-ERG tree.bark/vine get-PAST",
     "tree_bark_vine_get", 4, js(["warru", "warrur", "manil"]), js([]),
     "Warrur is tree bark usable as vine for fishnets; row states only getting the material.", "0.78"),
    (39, "The woman wove the bark strips.",
     "Jalbu-ngku ngunyin wukurrin.",
     "jalbu-ngku ngunyin wukurri-ny | woman-ERG bark.strips/dillybag weave-PAST",
     "bark_strip_weave", 4, js(["jalbu", "ngunyin", "wukurril"]), js([]),
     "Ngunyin can mean dillybag and bark used as strips for nets/bags; wukurril selects the craft sense.", "0.78"),
    (40, "The grass-tree resin is there on the spear.",
     "Kanunjul kalka-nga yinyay.",
     "kanunjul kalka-nga yinyay | grass.tree.resin spear-LOC there",
     "resin_on_spear", 4, js(["kanunjul", "kalka", "yinyay"]), js([]),
     "Kanunjul is resin/pitch used to seal spear bindings; row states only location on a spear.", "0.76"),
    (41, "The ti-tree bark lay at the shelter.",
     "Wanjakan dubal bayan-ba wunan.",
     "wanjakan dubal bayan-ba wuna-ny | ti.tree bark shelter-LOC lie-PAST",
     "ti_tree_bark_shelter_location", 4, js(["wanjakan", "dubal", "bayan", "wunay"]), js([]),
     "Wanjakan entry notes ti-tree bark used for roof/bark painting; row states only ti-tree bark at shelter.", "0.76"),
    (42, "I saw grass-tree resin.",
     "Ngayu kanunjul nyajin.",
     "ngayu kanunjul nyaji-ny | I grass.tree.resin see-PAST",
     "resin_see", 4, js(["ngayu", "kanunjul", "nyajil"]), js([]),
     "Second kanunjul row is an unelaborated recognition row because the entry has culturally loaded ritual prose.", "0.76"),

    (43, "I saw the bloodwood tree.",
     "Ngayu babajaka nyajin.",
     "ngayu babajaka nyaji-ny | I bloodwood.tree see-PAST",
     "bloodwood_tree_see", 4, js(["ngayu", "babajaka", "nyajil"]), js([]),
     "Babajaka is a bloodwood tree; no property beyond identification is asserted.", "0.80"),
    (44, "The Moreton Bay ash stands there.",
     "Bajar yinyay janay.",
     "bajar yinyay jana-y | Moreton.Bay.ash there stand-NONPAST",
     "ash_tree_stand", 4, js(["bajar", "yinyay", "janay"]), js([]),
     "Bajar is Moreton Bay ash/white gum; janay frames it as a standing tree, not residue ash.", "0.80"),
    (45, "The Moreton Bay fig stands there.",
     "Baril-baril yinyay janay.",
     "baril-baril yinyay jana-y | Moreton.Bay.fig there stand-NONPAST",
     "fig_tree_stand", 4, js(["baril-baril", "yinyay", "janay"]), js([]),
     "Baril-baril is a reduplicated Moreton Bay fig tree headword.", "0.80"),
    (46, "The black palm stands there.",
     "Duwar yinyay janay.",
     "duwar yinyay jana-y | black.palm there stand-NONPAST",
     "black_palm_stand", 4, js(["duwar", "yinyay", "janay"]), js([]),
     "Duwar is black palm; tree/palm sense selected by janay.", "0.80"),
    (47, "The water gum stands there.",
     "Jina-jina yinyay janay.",
     "jina-jina yinyay jana-y | water.gum there stand-NONPAST",
     "water_gum_stand", 4, js(["jina-jina", "yinyay", "janay"]), js([]),
     "Jina-jina is the hyphenated Yalanji water-gum form; unhyphenated jinajina was deliberately blocked.", "0.80"),
    (48, "The bottlebrush tree stands there.",
     "Yumu yinyay janay.",
     "yumu yinyay jana-y | bottlebrush.tree there stand-NONPAST",
     "bottlebrush_stand", 4, js(["yumu", "yinyay", "janay"]), js([]),
     "Yumu is bottlebrush tree; a simple stand row adds lexeme coverage.", "0.80"),
    (49, "The flowers are wilted.",
     "Nganka nganjay.",
     "nganka nganjay | flowers wilted/no.good",
     "flower_wilted_source", 5, js(["nganka", "nganjay"]), js([]),
     "Dictionary nganjay explicitly gives nganka nganjay as wilted flowers.", "0.86"),
    (50, "Many plant shoots are there.",
     "Jurrbu wubul yinyay.",
     "jurrbu wubul yinyay | plant.shoots many there",
     "plant_shoot_quantity", 4, js(["jurrbu", "wubul", "yinyay"]), js([]),
     "Jurrbu means shoots of plants; no species identification is invented.", "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 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 109 draft: plant/material lexicon", "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 109 source pass reread Rules 91/103/107/108 and the errata watchlist, then checked dictionary "
            "flora/material entries plus Patz exx.524-526, 544, 450-451, 483-484, 502, 606, 658, Text 51 lines "
            "6-13 and 61-67. Every content noun, verb, adjective, and case frame used in Batch 151 was checked "
            "against dictionary.yaml or grammar.md before banking."
        ),
        command="rg/sed over grammar.md and dictionary.yaml; sqlite exact-surface candidate check",
        artifact_path="process_logs/batch-151-plant-materials-process.md",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Scope is plant/material lexicon, not free ethnobotany. Avoided the conflicted barka entry, unsupported "
            "wankarr headword, garden loan in seed planting, unhyphenated jinajina, and any new generic botany taxonomy. "
            "Kept medicinal/ritual entries minimal: bangkan only boiled in water; kanunjul only seen or located on a spear; "
            "wanjakan bark only located at shelter."
        ),
        artifact_path="process_logs/batch-151-plant-materials-process.md",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="duplicate-preflight",
        actor="claude-main",
        detail=(
            "Exact-duplicate preflight first rejected five source-close candidates already accepted in earlier batches: "
            "Ngayu wawu jirray juku-ku birra-jirr-ka jabajaba-jirr-ka. (batch-130.25), Birra juku-muny "
            "yalbay-bajaku. (batch-133.02), Kabal mayi warrmba-bungan. (batch-133.17), Ngali juku-ngu dakal. "
            "(batch-133.18), and Dumbul marra-nga dandi. (batch-146.11). A second preflight rejected Mayi "
            "yinya-nga juku-ngu ngulkurr. (batch-133.03). Final 50 surfaces were duplicate-clean."
        ),
        command="python3 sqlite exact-surface candidate checks",
        artifact_path="process_logs/batch-151-plant-materials-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 109 plant/material lexical rows under_review after static and duplicate checks.",
        command="python3 batch_scripts/bank_151.py",
        artifact_path="batch_scripts/bank_151.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-12 cover plant-part and tree-part locative/ablative frames; 13-28 edible fruits, yams, roots, "
            "seeds, and source-backed preparation/planting frames; 29-42 fibre, bark, grass, paperbark, resin, and "
            "shelter/craft materials; 43-50 named tree, flower, and shoot lexemes."
        ),
        artifact_path="batch_scripts/bank_151.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())
