#!/usr/bin/env python3
"""Bank batch-272: Rule 230 second low-coverage named tree-reference lexical expansion."""

from __future__ import annotations

import datetime as dt
import json
import re
import sqlite3

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-272"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-272-second-low-coverage-named-tree-reference-lexical-expansion-after-rule229-process.md"

TARGET_HEADS = {
    "bingkajiri": "bingkajiri tree",
    "durral": "durral tree",
    "kalbariji": "kalbariji tree",
    "kalkuri": "kalkuri tree",
    "kuji-kuji": "kuji-kuji tree",
    "kuralbi": "kuralbi tree",
    "kuray-kuray": "kuray-kuray tree",
    "mingur": "mingur tree",
    "mukukana": "mukukana tree",
    "mungka-mungka": "mungka-mungka tree",
}
SUPPORT = {"yinya", "yaluy", "yinyay"}
DENY_KUKU = {
    # Earlier flora/tree exact targets from Rules 163, 165, 167, 199, 208, 224, and 225.
    "buyukal", "dalkan", "dari", "dibul", "dilbal", "dili", "jabarr", "jakari", "jama", "jilbar",
    "babajaka", "balmbalka", "baway", "bijin", "dalban", "dingki", "dulmbil", "duwar", "kalal", "kumul",
    "jina-jina", "jarrangkal", "julbin", "kunjarri", "kurruy", "malurri", "mawar", "maya", "wangay", "yawarr",
    "jinjajalka", "kurunkal", "madu", "marngkil", "marrabal", "milbarakal", "mungari", "wurmbariji",
    "wuymbariji", "kurrun", "banday", "bijan", "bukunjarra", "bulngkarr", "wanakan", "warka", "wulngkurr",
    "yalngkal", "yarun", "yumba", "jundul", "kabariji", "kawunjil", "kayjurr", "kingal", "kulbar", "kumin",
    "marbaji", "milbanku", "mujarr", "bindi", "dakil-dakil", "duduy", "jumbay-jumbay", "kababina",
    "kalal-kalal", "karul-karul", "malari", "waji-waji", "wayul",
    # Rejected or deferred candidates for this pass.
    "baril-baril", "bikarrakul", "bilar", "bujabay", "bulbul", "jilanji", "kajul", "kakaji", "karranda",
    "kawarr", "marra", "marrakan", "ngalkarran", "ngurrkul", "yinkunji",
}
DENY_ENGLISH = (
    "fruit", "seed", "edible", "food", "eat", "eating", "ate", "roast", "roasted", "cook", "cooked",
    "blind", "spear", "spears", "rope", "bark", "medicine", "poison", "poisonous", "sap", "timber",
    "wood", "material", "use", "used", "flower", "season", "ripe", "thorn", "thorns", "branch",
    "branches", "habitat", "ecology", "country", "story", "place", "water", "gum", "nut", "nuts",
)


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


def row(
    seq: int,
    english: str,
    kuku: str,
    analysis: str,
    frame: str,
    words: list[str],
    evidence: str,
) -> tuple:
    return (seq, english, kuku, analysis, frame, 2, js(words), js([]), evidence, "0.86")


EVIDENCE = {
    "bingkajiri": "Dictionary bingkajiri noun gloss 'bingkajiri tree', a flora-tree entry. Patz section 3.1 includes flora nouns; section 3.7 supports yinya/yaluy/yinyay.",
    "durral": "Dictionary durral noun gloss 'durral tree', a flora-tree entry. Food/wood/spear prose is closed. Patz 3.1 flora noun and 3.7 deictic support.",
    "kalbariji": "Dictionary kalbariji noun gloss 'tree' with definition 'finger cherry', a flora-tree entry. Batch uses kalbariji-tree reference only. Patz 3.1 and 3.7.",
    "kalkuri": "Dictionary kalkuri noun gloss 'tree', definition Melaleuca gum/woollybutt, a flora-tree entry. Batch uses kalkuri-tree reference only. Patz 3.1 and 3.7.",
    "kuji-kuji": "Dictionary kuji-kuji noun gloss 'tree', lexicalized hyphenated reduplication and species of bloodwood, a flora-tree entry. Patz 3.1 and 3.7.",
    "kuralbi": "Dictionary kuralbi noun gloss 'tree', species name unknown. Prior corpus has kuralbi bark-source rows, but this batch opens only bare tree reference. Patz 3.1 and 3.7.",
    "kuray-kuray": "Dictionary kuray-kuray noun gloss 'tree', milka tree, lexicalized hyphenated reduplication. Patz 3.1 and 3.7.",
    "mingur": "Dictionary mingur noun gloss 'tree', oilnut tree, a flora-tree entry. Batch uses mingur-tree reference only. Patz 3.1 and 3.7.",
    "mukukana": "Dictionary mukukana noun gloss 'tree', species with edible fruit, a flora-tree entry. Food/fruit prose is closed. Patz 3.1 and 3.7.",
    "mungka-mungka": "Dictionary mungka-mungka noun gloss 'tree', small tree with thorns, lexicalized hyphenated reduplication. Thorn/branch prose is closed. Patz 3.1 and 3.7.",
}


ROWS = [
    row(1, "The bingkajiri tree is here.", "Bingkajiri yaluy.", "bingkajiri yaluy | bingkajiri.tree here", "r230_tree_ref_here_bingkajiri", ["bingkajiri", "yaluy"], EVIDENCE["bingkajiri"]),
    row(2, "The durral tree is here.", "Durral yaluy.", "durral yaluy | durral.tree here", "r230_tree_ref_here_durral", ["durral", "yaluy"], EVIDENCE["durral"]),
    row(3, "The kalbariji tree is here.", "Kalbariji yaluy.", "kalbariji yaluy | kalbariji.tree here", "r230_tree_ref_here_kalbariji", ["kalbariji", "yaluy"], EVIDENCE["kalbariji"]),
    row(4, "The kalkuri tree is here.", "Kalkuri yaluy.", "kalkuri yaluy | kalkuri.tree here", "r230_tree_ref_here_kalkuri", ["kalkuri", "yaluy"], EVIDENCE["kalkuri"]),
    row(5, "The kuji-kuji tree is here.", "Kuji-kuji yaluy.", "kuji-kuji yaluy | kuji.kuji.tree here", "r230_tree_ref_here_kuji_kuji", ["kuji-kuji", "yaluy"], EVIDENCE["kuji-kuji"]),
    row(6, "The kuralbi tree is here.", "Kuralbi yaluy.", "kuralbi yaluy | kuralbi.tree here", "r230_tree_ref_here_kuralbi", ["kuralbi", "yaluy"], EVIDENCE["kuralbi"]),
    row(7, "The kuray-kuray tree is here.", "Kuray-kuray yaluy.", "kuray-kuray yaluy | kuray.kuray.tree here", "r230_tree_ref_here_kuray_kuray", ["kuray-kuray", "yaluy"], EVIDENCE["kuray-kuray"]),
    row(8, "The mingur tree is here.", "Mingur yaluy.", "mingur yaluy | mingur.tree here", "r230_tree_ref_here_mingur", ["mingur", "yaluy"], EVIDENCE["mingur"]),
    row(9, "The mukukana tree is here.", "Mukukana yaluy.", "mukukana yaluy | mukukana.tree here", "r230_tree_ref_here_mukukana", ["mukukana", "yaluy"], EVIDENCE["mukukana"]),
    row(10, "The mungka-mungka tree is here.", "Mungka-mungka yaluy.", "mungka-mungka yaluy | mungka.mungka.tree here", "r230_tree_ref_here_mungka_mungka", ["mungka-mungka", "yaluy"], EVIDENCE["mungka-mungka"]),
    row(11, "The bingkajiri tree is there.", "Bingkajiri yinyay.", "bingkajiri yinyay | bingkajiri.tree there", "r230_tree_ref_there_bingkajiri", ["bingkajiri", "yinyay"], EVIDENCE["bingkajiri"]),
    row(12, "The durral tree is there.", "Durral yinyay.", "durral yinyay | durral.tree there", "r230_tree_ref_there_durral", ["durral", "yinyay"], EVIDENCE["durral"]),
    row(13, "The kalbariji tree is there.", "Kalbariji yinyay.", "kalbariji yinyay | kalbariji.tree there", "r230_tree_ref_there_kalbariji", ["kalbariji", "yinyay"], EVIDENCE["kalbariji"]),
    row(14, "The kalkuri tree is there.", "Kalkuri yinyay.", "kalkuri yinyay | kalkuri.tree there", "r230_tree_ref_there_kalkuri", ["kalkuri", "yinyay"], EVIDENCE["kalkuri"]),
    row(15, "The kuji-kuji tree is there.", "Kuji-kuji yinyay.", "kuji-kuji yinyay | kuji.kuji.tree there", "r230_tree_ref_there_kuji_kuji", ["kuji-kuji", "yinyay"], EVIDENCE["kuji-kuji"]),
    row(16, "The kuralbi tree is there.", "Kuralbi yinyay.", "kuralbi yinyay | kuralbi.tree there", "r230_tree_ref_there_kuralbi", ["kuralbi", "yinyay"], EVIDENCE["kuralbi"]),
    row(17, "The kuray-kuray tree is there.", "Kuray-kuray yinyay.", "kuray-kuray yinyay | kuray.kuray.tree there", "r230_tree_ref_there_kuray_kuray", ["kuray-kuray", "yinyay"], EVIDENCE["kuray-kuray"]),
    row(18, "The mingur tree is there.", "Mingur yinyay.", "mingur yinyay | mingur.tree there", "r230_tree_ref_there_mingur", ["mingur", "yinyay"], EVIDENCE["mingur"]),
    row(19, "The mukukana tree is there.", "Mukukana yinyay.", "mukukana yinyay | mukukana.tree there", "r230_tree_ref_there_mukukana", ["mukukana", "yinyay"], EVIDENCE["mukukana"]),
    row(20, "The mungka-mungka tree is there.", "Mungka-mungka yinyay.", "mungka-mungka yinyay | mungka.mungka.tree there", "r230_tree_ref_there_mungka_mungka", ["mungka-mungka", "yinyay"], EVIDENCE["mungka-mungka"]),
    row(21, "That bingkajiri tree is there.", "Yinya bingkajiri yinyay.", "yinya bingkajiri yinyay | that bingkajiri.tree there", "r230_that_tree_ref_there_bingkajiri", ["yinya", "bingkajiri", "yinyay"], EVIDENCE["bingkajiri"]),
    row(22, "That durral tree is there.", "Yinya durral yinyay.", "yinya durral yinyay | that durral.tree there", "r230_that_tree_ref_there_durral", ["yinya", "durral", "yinyay"], EVIDENCE["durral"]),
    row(23, "That kalbariji tree is there.", "Yinya kalbariji yinyay.", "yinya kalbariji yinyay | that kalbariji.tree there", "r230_that_tree_ref_there_kalbariji", ["yinya", "kalbariji", "yinyay"], EVIDENCE["kalbariji"]),
    row(24, "That kalkuri tree is there.", "Yinya kalkuri yinyay.", "yinya kalkuri yinyay | that kalkuri.tree there", "r230_that_tree_ref_there_kalkuri", ["yinya", "kalkuri", "yinyay"], EVIDENCE["kalkuri"]),
    row(25, "That kuji-kuji tree is there.", "Yinya kuji-kuji yinyay.", "yinya kuji-kuji yinyay | that kuji.kuji.tree there", "r230_that_tree_ref_there_kuji_kuji", ["yinya", "kuji-kuji", "yinyay"], EVIDENCE["kuji-kuji"]),
    row(26, "That kuralbi tree is there.", "Yinya kuralbi yinyay.", "yinya kuralbi yinyay | that kuralbi.tree there", "r230_that_tree_ref_there_kuralbi", ["yinya", "kuralbi", "yinyay"], EVIDENCE["kuralbi"]),
    row(27, "That kuray-kuray tree is there.", "Yinya kuray-kuray yinyay.", "yinya kuray-kuray yinyay | that kuray.kuray.tree there", "r230_that_tree_ref_there_kuray_kuray", ["yinya", "kuray-kuray", "yinyay"], EVIDENCE["kuray-kuray"]),
    row(28, "That mingur tree is there.", "Yinya mingur yinyay.", "yinya mingur yinyay | that mingur.tree there", "r230_that_tree_ref_there_mingur", ["yinya", "mingur", "yinyay"], EVIDENCE["mingur"]),
    row(29, "That mukukana tree is there.", "Yinya mukukana yinyay.", "yinya mukukana yinyay | that mukukana.tree there", "r230_that_tree_ref_there_mukukana", ["yinya", "mukukana", "yinyay"], EVIDENCE["mukukana"]),
    row(30, "That mungka-mungka tree is there.", "Yinya mungka-mungka yinyay.", "yinya mungka-mungka yinyay | that mungka.mungka.tree there", "r230_that_tree_ref_there_mungka_mungka", ["yinya", "mungka-mungka", "yinyay"], EVIDENCE["mungka-mungka"]),
    row(31, "That bingkajiri tree is there.", "Bingkajiri yinya yinyay.", "bingkajiri yinya yinyay | that bingkajiri.tree there", "r230_tree_that_there_bingkajiri", ["bingkajiri", "yinya", "yinyay"], EVIDENCE["bingkajiri"]),
    row(32, "That durral tree is there.", "Durral yinya yinyay.", "durral yinya yinyay | that durral.tree there", "r230_tree_that_there_durral", ["durral", "yinya", "yinyay"], EVIDENCE["durral"]),
    row(33, "That kalbariji tree is there.", "Kalbariji yinya yinyay.", "kalbariji yinya yinyay | that kalbariji.tree there", "r230_tree_that_there_kalbariji", ["kalbariji", "yinya", "yinyay"], EVIDENCE["kalbariji"]),
    row(34, "That kalkuri tree is there.", "Kalkuri yinya yinyay.", "kalkuri yinya yinyay | that kalkuri.tree there", "r230_tree_that_there_kalkuri", ["kalkuri", "yinya", "yinyay"], EVIDENCE["kalkuri"]),
    row(35, "That kuji-kuji tree is there.", "Kuji-kuji yinya yinyay.", "kuji-kuji yinya yinyay | that kuji.kuji.tree there", "r230_tree_that_there_kuji_kuji", ["kuji-kuji", "yinya", "yinyay"], EVIDENCE["kuji-kuji"]),
    row(36, "That kuralbi tree is there.", "Kuralbi yinya yinyay.", "kuralbi yinya yinyay | that kuralbi.tree there", "r230_tree_that_there_kuralbi", ["kuralbi", "yinya", "yinyay"], EVIDENCE["kuralbi"]),
    row(37, "That kuray-kuray tree is there.", "Kuray-kuray yinya yinyay.", "kuray-kuray yinya yinyay | that kuray.kuray.tree there", "r230_tree_that_there_kuray_kuray", ["kuray-kuray", "yinya", "yinyay"], EVIDENCE["kuray-kuray"]),
    row(38, "That mingur tree is there.", "Mingur yinya yinyay.", "mingur yinya yinyay | that mingur.tree there", "r230_tree_that_there_mingur", ["mingur", "yinya", "yinyay"], EVIDENCE["mingur"]),
    row(39, "That mukukana tree is there.", "Mukukana yinya yinyay.", "mukukana yinya yinyay | that mukukana.tree there", "r230_tree_that_there_mukukana", ["mukukana", "yinya", "yinyay"], EVIDENCE["mukukana"]),
    row(40, "That mungka-mungka tree is there.", "Mungka-mungka yinya yinyay.", "mungka-mungka yinya yinyay | that mungka.mungka.tree there", "r230_tree_that_there_mungka_mungka", ["mungka-mungka", "yinya", "yinyay"], EVIDENCE["mungka-mungka"]),
    row(41, "That bingkajiri tree is here.", "Yinya bingkajiri yaluy.", "yinya bingkajiri yaluy | that bingkajiri.tree here", "r230_that_tree_ref_here_bingkajiri", ["yinya", "bingkajiri", "yaluy"], EVIDENCE["bingkajiri"]),
    row(42, "That durral tree is here.", "Yinya durral yaluy.", "yinya durral yaluy | that durral.tree here", "r230_that_tree_ref_here_durral", ["yinya", "durral", "yaluy"], EVIDENCE["durral"]),
    row(43, "That kalbariji tree is here.", "Yinya kalbariji yaluy.", "yinya kalbariji yaluy | that kalbariji.tree here", "r230_that_tree_ref_here_kalbariji", ["yinya", "kalbariji", "yaluy"], EVIDENCE["kalbariji"]),
    row(44, "That kalkuri tree is here.", "Yinya kalkuri yaluy.", "yinya kalkuri yaluy | that kalkuri.tree here", "r230_that_tree_ref_here_kalkuri", ["yinya", "kalkuri", "yaluy"], EVIDENCE["kalkuri"]),
    row(45, "That kuji-kuji tree is here.", "Yinya kuji-kuji yaluy.", "yinya kuji-kuji yaluy | that kuji.kuji.tree here", "r230_that_tree_ref_here_kuji_kuji", ["yinya", "kuji-kuji", "yaluy"], EVIDENCE["kuji-kuji"]),
    row(46, "That kuralbi tree is here.", "Yinya kuralbi yaluy.", "yinya kuralbi yaluy | that kuralbi.tree here", "r230_that_tree_ref_here_kuralbi", ["yinya", "kuralbi", "yaluy"], EVIDENCE["kuralbi"]),
    row(47, "That kuray-kuray tree is here.", "Yinya kuray-kuray yaluy.", "yinya kuray-kuray yaluy | that kuray.kuray.tree here", "r230_that_tree_ref_here_kuray_kuray", ["yinya", "kuray-kuray", "yaluy"], EVIDENCE["kuray-kuray"]),
    row(48, "That mingur tree is here.", "Yinya mingur yaluy.", "yinya mingur yaluy | that mingur.tree here", "r230_that_tree_ref_here_mingur", ["yinya", "mingur", "yaluy"], EVIDENCE["mingur"]),
    row(49, "That mukukana tree is here.", "Yinya mukukana yaluy.", "yinya mukukana yaluy | that mukukana.tree here", "r230_that_tree_ref_here_mukukana", ["yinya", "mukukana", "yaluy"], EVIDENCE["mukukana"]),
    row(50, "That mungka-mungka tree is here.", "Yinya mungka-mungka yaluy.", "yinya mungka-mungka yaluy | that mungka.mungka.tree here", "r230_that_tree_ref_here_mungka_mungka", ["yinya", "mungka-mungka", "yaluy"], EVIDENCE["mungka-mungka"]),
]


def has_token(low: str, token: str) -> bool:
    return re.search(rf"(?<![a-z-]){re.escape(token)}(?![a-z-])", low) is not None


def has_target_morphology(low: str, head: str) -> bool:
    suffixes = "ngka|ngku|angka|nga|ngu|ba|mba|mbu|muny|darr|burr|bu|ka|nka|nku|ji|jirr|ku|mundu|amu|mu|iji|nda|ndu|anda|undu|unji|nji|kari|baka|baja|bungal|bungan|mal|man|rriny|karra|manda|bulal"
    return re.search(rf"(?<![a-z-]){re.escape(head)}-(?:{suffixes})\b", low) is not None


def english_has_denied(english_low: str, denied: str) -> bool:
    return denied in english_low if " " in denied else re.search(rf"\b{re.escape(denied.strip())}\b", english_low) is not None


def scan_static() -> list[str]:
    issues: list[str] = []
    if len(ROWS) != 50:
        issues.append(f"{LABEL}: expected 50 rows, found {len(ROWS)}")
    seen_ku: set[str] = set()
    for expected, row_data in enumerate(ROWS, start=1):
        seq, english, kuku, analysis, frame, _tier, words_used, loans_used, evidence, confidence = row_data
        label = f"{LABEL}.{seq:02d}"
        low = kuku.lower()
        english_low = english.lower()
        words = json.loads(words_used)
        if seq != expected:
            issues.append(f"{label}: sequence mismatch; expected {expected}")
        if not english[:1].isupper() or english[-1:] not in ".!?":
            issues.append(f"{label}: English punctuation/capitalization failure")
        if " | " not in analysis:
            issues.append(f"{label}: analysis missing gloss separator")
        if not frame.startswith("r230_"):
            issues.append(f"{label}: Rule 230 frame must start with r230_")
        if low in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface within batch")
        seen_ku.add(low)
        if json.loads(loans_used):
            issues.append(f"{label}: no loans are admitted in Rule 230")
        for denied in DENY_KUKU:
            if has_token(low, denied):
                issues.append(f"{label}: denied earlier flora/tree token {denied!r}")
        targets = [head for head in TARGET_HEADS if has_token(low, head)]
        if len(targets) != 1:
            issues.append(f"{label}: expected exactly one Rule 230 target noun, found {targets}")
        for head in targets:
            selected = TARGET_HEADS[head]
            dotted = selected.replace("-", ".").replace(" ", ".")
            if head not in words:
                issues.append(f"{label}: target head {head} missing from words_used")
            if selected not in english_low:
                issues.append(f"{label}: English must include selected sense {selected!r}")
            if selected not in analysis and dotted not in analysis:
                issues.append(f"{label}: analysis must gloss {head} as {selected}")
            if has_target_morphology(low, head):
                issues.append(f"{label}: target head {head} has forbidden morphology")
        for support in SUPPORT:
            if has_token(low, support) and support not in words:
                issues.append(f"{label}: support {support} missing from words_used")
        if 1 <= seq <= 10 and (has_token(low, "yinya") or has_token(low, "yinyay") or not low.endswith(" yaluy.") or len(low.split()) != 2):
            issues.append(f"{label}: rows 1-10 require TARGET yaluy")
        if 11 <= seq <= 20 and (has_token(low, "yinya") or has_token(low, "yaluy") or not low.endswith(" yinyay.") or len(low.split()) != 2):
            issues.append(f"{label}: rows 11-20 require TARGET yinyay")
        if 21 <= seq <= 30 and (not low.startswith("yinya ") or has_token(low, "yaluy") or not low.endswith(" yinyay.") or len(low.split()) != 3):
            issues.append(f"{label}: rows 21-30 require Yinya TARGET yinyay")
        if 31 <= seq <= 40 and (not has_token(low, "yinya") or has_token(low, "yaluy") or not low.endswith(" yinyay.") or len(low.split()) != 3 or low.startswith("yinya ")):
            issues.append(f"{label}: rows 31-40 require TARGET yinya yinyay")
        if 41 <= seq <= 50 and (not low.startswith("yinya ") or has_token(low, "yinyay") or not low.endswith(" yaluy.") or len(low.split()) != 3):
            issues.append(f"{label}: rows 41-50 require Yinya TARGET yaluy")
        if any(english_has_denied(english_low, bad) for bad in DENY_ENGLISH):
            issues.append(f"{label}: denied tree-source-prose field in English")
        if not evidence or not confidence:
            issues.append(f"{label}: missing evidence/confidence")
    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,))
    else:
        db.execute(
            "INSERT INTO batches(label, cheatsheet_version, guide_version, notes, created_at) VALUES(?,?,?,?,?)",
            (LABEL, "Rule 230 draft", "operator-guide-2026-07-08", "draft under review", NOW),
        )
        batch_id = db.execute("SELECT id FROM batches WHERE label=?", (LABEL,)).fetchone()[0]

    collisions: list[str] = []
    for seq, _english, kuku, *_rest in ROWS:
        found = db.execute(
            """
            SELECT b.label, s.seq, s.status
            FROM sentences s JOIN batches b ON b.id=s.batch_id
            WHERE lower(s.kuku)=lower(?) AND s.status IN ('accepted','revised','under_review')
            """,
            (kuku,),
        ).fetchall()
        if found:
            collisions.append(f"{seq:02d} {kuku}: {found}")
    if collisions:
        raise SystemExit("duplicate preflight failed:\n" + "\n".join(collisions))

    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="field-audit",
        actor="claude-main",
        detail="Rule 230 first audited insects, then shellfish/crustaceans. Insects lacked ten clean unused low-risk heads after Rules 169, 212, and 222; shellfish/crustaceans lacked ten clean unused heads after Rules 171, 213, and 223 plus lint-failing jirrum. The batch therefore moved to a source-backed named-tree field.",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="source-pass",
        actor="claude-main",
        detail="Checked Patz section 3.1 flora-noun basis, Patz section 3.7 yinya/yaluy/yinyay support, Rules 199/208/224 named tree-reference law, and dictionary entries for bingkajiri, durral, kalbariji, kalkuri, kuji-kuji, kuralbi, kuray-kuray, mingur, mukukana, and mungka-mungka.",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="candidate-rejection",
        actor="claude-main",
        detail="Selected ten low-count named-tree heads after excluding earlier flora/tree exact targets, no-definition entries, high-count/homograph entries, and candidates whose source prose would make fruit, food, seed, medicine, poison, bark, material, habitat, place, or story drift too likely.",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="duplicate-preflight",
        actor="claude-main",
        detail="Exact duplicate preflight over all 50 planned Rule 230 Kuku surfaces returned no accepted, revised, or under-review collisions.",
        command="sqlite3 exact lower(kuku) collision scan for 50 planned surfaces",
        output_excerpt="collisions=0",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="static-guard-pass",
        actor="claude-main",
        detail="bank_272.py static guards passed: 50 explicit hand-authored rows, exactly one Rule 230 named-tree target noun per row, no loans, no target morphology, no earlier flora/tree target token, no tree source-prose English, and only five sourced deictic frame bands.",
        artifact_path="batch_scripts/bank_272.py",
        created_at=NOW,
    )

    db.executemany(
        """
        INSERT INTO sentences(
          batch_id, seq, english, kuku, analysis, frame, tier, words_used,
          loans_used, evidence, status, confidence, created_at, updated_at
        )
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        [
            (batch_id, seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, "under_review", confidence, NOW, NOW)
            for seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, confidence in ROWS
        ],
    )
    db.commit()

    n = db.execute("SELECT COUNT(*) FROM sentences WHERE batch_id=? AND status='under_review'", (batch_id,)).fetchone()[0]
    print(f"banked {LABEL}: {n} under_review rows")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
