#!/usr/bin/env python3
"""Bank batch-214: Rule 172 construction consolidation after Rule 171."""

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-214"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-214-construction-consolidation-after-rule171-process.md"

TARGET_HEADS = {
    "balungkul": "shell back snail",
    "juwarru": "mud oyster",
    "marakal": "spider shell",
    "marku": "species of oyster",
    "marubu": "species of oyster",
    "munji": "big clam shell",
    "ngulumuku": "seashell",
    "wukuju": "freshwater prawn",
    "wuybur": "jellyfish",
    "yulba": "large round oyster",
}
PRONOUN_A = {"ngayu", "yundu", "jana", "ngana"}
DENY_KUKU = {
    "baymbay", "dakaldakal", "darkay", "jalan", "julul", "kaduy", "kajarrbi", "karraway",
    "karruku", "kiju", "kulil", "kunkun", "kurrija", "kuyalin", "jirrum", "marrkabina",
    "mukirr", "ngajay", "ngalngal", "ngarrakaja", "nikar", "nuwi", "wakal", "wuli-wuli",
    "wurru-bulan", "wurrul", "mili", "manin", "bangkan", "nuka", "nukan", "waju", "wajun",
    "daya", "dajin", "daji", "dinda", "dindan", "wandil", "wandin", "bayka", "baykal",
}
DENY_ENGLISH = (
    "edible", "eat", "eats", "ate", "eaten", "food", "meat", "bait", "catch", "catches",
    "caught", "gather", "gathers", "gathered", "cook", "cooks", "cooked", "fry", "fried",
    "roast", "roasted", "boil", "boiled", "clean", "cleaned", "tobacco", "rock", "rocks",
    "mangrove", "mangroves", "cling", "clings", "clung", "night", "nocturnal", "hole", "holes",
    "leg", "legs", "touch", "sting", "stings", "stung", "danger", "dangerous", "preparation",
)


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, 4, js(words), js([]), evidence, "0.84")


ROWS = [
    row(1, "The shell back snail is there.", "Balungkul yinyay.", "balungkul yinyay | shell.back.snail there", "r172_deictic_balungkul_there", ["balungkul", "yinyay"], "Rule 172 retains Rule 171 balungkul as shell back snail; bare yinyay consolidation."),
    row(2, "The marku species of oyster is there.", "Marku yinyay.", "marku yinyay | species.of.oyster there", "r172_deictic_marku_there", ["marku", "yinyay"], "Rule 172 retains Rule 171 marku as species of oyster; bare yinyay consolidation."),
    row(3, "The mud oyster is there.", "Juwarru yinyay.", "juwarru yinyay | mud.oyster there", "r172_deictic_juwarru_there", ["juwarru", "yinyay"], "Rule 172 retains Rule 171 juwarru as mud oyster; no habitat or comparison row."),
    row(4, "The spider shell is there.", "Marakal yinyay.", "marakal yinyay | spider.shell there", "r172_deictic_marakal_there", ["marakal", "yinyay"], "Rule 172 retains Rule 171 marakal as spider shell; reference only."),
    row(5, "The marubu species of oyster is there.", "Marubu yinyay.", "marubu yinyay | species.of.oyster there", "r172_deictic_marubu_there", ["marubu", "yinyay"], "Rule 172 retains Rule 171 marubu as species of oyster; reference only."),
    row(6, "The big clam shell is there.", "Munji yinyay.", "munji yinyay | big.clam.shell there", "r172_deictic_munji_there", ["munji", "yinyay"], "Rule 172 retains Rule 171 munji as big clam shell; no use row."),
    row(7, "The seashell is there.", "Ngulumuku yinyay.", "ngulumuku yinyay | seashell there", "r172_deictic_ngulumuku_there", ["ngulumuku", "yinyay"], "Rule 172 retains Rule 171 ngulumuku as seashell; reference only."),
    row(8, "The freshwater prawn is there.", "Wukuju yinyay.", "wukuju yinyay | freshwater.prawn there", "r172_deictic_wukuju_there", ["wukuju", "yinyay"], "Rule 172 retains Rule 171 wukuju as freshwater prawn; no size/leg row."),
    row(9, "The jellyfish is there.", "Wuybur yinyay.", "wuybur yinyay | jellyfish there", "r172_deictic_wuybur_there", ["wuybur", "yinyay"], "Rule 172 retains Rule 171 wuybur as jellyfish; no sting/touch row."),
    row(10, "The large round oyster is there.", "Yulba yinyay.", "yulba yinyay | large.round.oyster there", "r172_deictic_yulba_there", ["yulba", "yinyay"], "Rule 172 retains Rule 171 yulba as large round oyster; reference only."),
    row(11, "I see the shell back snail here.", "Ngayu balungkul yaluy nyajil.", "ngayu balungkul yaluy nyaji-l | I shell.back.snail here see-NONPAST", "r172_present_i_balungkul_here", ["ngayu", "balungkul", "yaluy", "nyaji-l"], "Pronoun A bare; balungkul object bare; yaluy support only."),
    row(12, "I see the marku species of oyster here.", "Ngayu marku yaluy nyajil.", "ngayu marku yaluy nyaji-l | I species.of.oyster here see-NONPAST", "r172_present_i_marku_here", ["ngayu", "marku", "yaluy", "nyaji-l"], "Pronoun A bare; marku object bare; yaluy support only."),
    row(13, "I see the mud oyster here.", "Ngayu juwarru yaluy nyajil.", "ngayu juwarru yaluy nyaji-l | I mud.oyster here see-NONPAST", "r172_present_i_juwarru_here", ["ngayu", "juwarru", "yaluy", "nyaji-l"], "Pronoun A bare; no habitat/comparison row."),
    row(14, "I see the spider shell here.", "Ngayu marakal yaluy nyajil.", "ngayu marakal yaluy nyaji-l | I spider.shell here see-NONPAST", "r172_present_i_marakal_here", ["ngayu", "marakal", "yaluy", "nyaji-l"], "Pronoun A bare; marakal object bare."),
    row(15, "I see the marubu species of oyster here.", "Ngayu marubu yaluy nyajil.", "ngayu marubu yaluy nyaji-l | I species.of.oyster here see-NONPAST", "r172_present_i_marubu_here", ["ngayu", "marubu", "yaluy", "nyaji-l"], "Pronoun A bare; marubu object bare."),
    row(16, "I see the big clam shell here.", "Ngayu munji yaluy nyajil.", "ngayu munji yaluy nyaji-l | I big.clam.shell here see-NONPAST", "r172_present_i_munji_here", ["ngayu", "munji", "yaluy", "nyaji-l"], "Pronoun A bare; no use row."),
    row(17, "I see the seashell here.", "Ngayu ngulumuku yaluy nyajil.", "ngayu ngulumuku yaluy nyaji-l | I seashell here see-NONPAST", "r172_present_i_ngulumuku_here", ["ngayu", "ngulumuku", "yaluy", "nyaji-l"], "Pronoun A bare; ngulumuku object bare."),
    row(18, "I see the freshwater prawn here.", "Ngayu wukuju yaluy nyajil.", "ngayu wukuju yaluy nyaji-l | I freshwater.prawn here see-NONPAST", "r172_present_i_wukuju_here", ["ngayu", "wukuju", "yaluy", "nyaji-l"], "Pronoun A bare; no size/leg row."),
    row(19, "I see the jellyfish here.", "Ngayu wuybur yaluy nyajil.", "ngayu wuybur yaluy nyaji-l | I jellyfish here see-NONPAST", "r172_present_i_wuybur_here", ["ngayu", "wuybur", "yaluy", "nyaji-l"], "Pronoun A bare; no sting/touch row."),
    row(20, "I see the large round oyster here.", "Ngayu yulba yaluy nyajil.", "ngayu yulba yaluy nyaji-l | I large.round.oyster here see-NONPAST", "r172_present_i_yulba_here", ["ngayu", "yulba", "yaluy", "nyaji-l"], "Pronoun A bare; yulba object bare."),
    row(21, "You see the shell back snail here.", "Yundu balungkul yaluy nyajil.", "yundu balungkul yaluy nyaji-l | you shell.back.snail here see-NONPAST", "r172_present_you_balungkul_here", ["yundu", "balungkul", "yaluy", "nyaji-l"], "Second-person pronoun A bare; yaluy support only."),
    row(22, "You see the marku species of oyster here.", "Yundu marku yaluy nyajil.", "yundu marku yaluy nyaji-l | you species.of.oyster here see-NONPAST", "r172_present_you_marku_here", ["yundu", "marku", "yaluy", "nyaji-l"], "Second-person pronoun A bare; target object bare."),
    row(23, "You see the mud oyster here.", "Yundu juwarru yaluy nyajil.", "yundu juwarru yaluy nyaji-l | you mud.oyster here see-NONPAST", "r172_present_you_juwarru_here", ["yundu", "juwarru", "yaluy", "nyaji-l"], "Second-person pronoun A bare; no habitat/comparison row."),
    row(24, "You see the spider shell here.", "Yundu marakal yaluy nyajil.", "yundu marakal yaluy nyaji-l | you spider.shell here see-NONPAST", "r172_present_you_marakal_here", ["yundu", "marakal", "yaluy", "nyaji-l"], "Second-person pronoun A bare; target object bare."),
    row(25, "You see the marubu species of oyster here.", "Yundu marubu yaluy nyajil.", "yundu marubu yaluy nyaji-l | you species.of.oyster here see-NONPAST", "r172_present_you_marubu_here", ["yundu", "marubu", "yaluy", "nyaji-l"], "Second-person pronoun A bare; target object bare."),
    row(26, "You see the big clam shell here.", "Yundu munji yaluy nyajil.", "yundu munji yaluy nyaji-l | you big.clam.shell here see-NONPAST", "r172_present_you_munji_here", ["yundu", "munji", "yaluy", "nyaji-l"], "Second-person pronoun A bare; no use row."),
    row(27, "You see the seashell here.", "Yundu ngulumuku yaluy nyajil.", "yundu ngulumuku yaluy nyaji-l | you seashell here see-NONPAST", "r172_present_you_ngulumuku_here", ["yundu", "ngulumuku", "yaluy", "nyaji-l"], "Second-person pronoun A bare; target object bare."),
    row(28, "You see the freshwater prawn here.", "Yundu wukuju yaluy nyajil.", "yundu wukuju yaluy nyaji-l | you freshwater.prawn here see-NONPAST", "r172_present_you_wukuju_here", ["yundu", "wukuju", "yaluy", "nyaji-l"], "Second-person pronoun A bare; no size/leg row."),
    row(29, "You see the jellyfish here.", "Yundu wuybur yaluy nyajil.", "yundu wuybur yaluy nyaji-l | you jellyfish here see-NONPAST", "r172_present_you_wuybur_here", ["yundu", "wuybur", "yaluy", "nyaji-l"], "Second-person pronoun A bare; no sting/touch row."),
    row(30, "You see the large round oyster here.", "Yundu yulba yaluy nyajil.", "yundu yulba yaluy nyaji-l | you large.round.oyster here see-NONPAST", "r172_present_you_yulba_here", ["yundu", "yulba", "yaluy", "nyaji-l"], "Second-person pronoun A bare; target object bare."),
    row(31, "They saw the shell back snail here.", "Jana balungkul yaluy nyajin.", "jana balungkul yaluy nyaji-ny | they shell.back.snail here see-PAST", "r172_past_they_balungkul_here", ["jana", "balungkul", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; past perception; yaluy support only."),
    row(32, "They saw the marku species of oyster here.", "Jana marku yaluy nyajin.", "jana marku yaluy nyaji-ny | they species.of.oyster here see-PAST", "r172_past_they_marku_here", ["jana", "marku", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; past perception."),
    row(33, "They saw the mud oyster here.", "Jana juwarru yaluy nyajin.", "jana juwarru yaluy nyaji-ny | they mud.oyster here see-PAST", "r172_past_they_juwarru_here", ["jana", "juwarru", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; no habitat/comparison row."),
    row(34, "They saw the spider shell here.", "Jana marakal yaluy nyajin.", "jana marakal yaluy nyaji-ny | they spider.shell here see-PAST", "r172_past_they_marakal_here", ["jana", "marakal", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; target bare."),
    row(35, "They saw the marubu species of oyster here.", "Jana marubu yaluy nyajin.", "jana marubu yaluy nyaji-ny | they species.of.oyster here see-PAST", "r172_past_they_marubu_here", ["jana", "marubu", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; target bare."),
    row(36, "They saw the big clam shell here.", "Jana munji yaluy nyajin.", "jana munji yaluy nyaji-ny | they big.clam.shell here see-PAST", "r172_past_they_munji_here", ["jana", "munji", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; no use row."),
    row(37, "They saw the seashell here.", "Jana ngulumuku yaluy nyajin.", "jana ngulumuku yaluy nyaji-ny | they seashell here see-PAST", "r172_past_they_ngulumuku_here", ["jana", "ngulumuku", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; target bare."),
    row(38, "They saw the freshwater prawn here.", "Jana wukuju yaluy nyajin.", "jana wukuju yaluy nyaji-ny | they freshwater.prawn here see-PAST", "r172_past_they_wukuju_here", ["jana", "wukuju", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; no size/leg row."),
    row(39, "They saw the jellyfish here.", "Jana wuybur yaluy nyajin.", "jana wuybur yaluy nyaji-ny | they jellyfish here see-PAST", "r172_past_they_wuybur_here", ["jana", "wuybur", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; no sting/touch row."),
    row(40, "They saw the large round oyster here.", "Jana yulba yaluy nyajin.", "jana yulba yaluy nyaji-ny | they large.round.oyster here see-PAST", "r172_past_they_yulba_here", ["jana", "yulba", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; target bare."),
    row(41, "We see the shell back snail there.", "Ngana balungkul yinyay nyajil.", "ngana balungkul yinyay nyaji-l | we shell.back.snail there see-NONPAST", "r172_present_we_balungkul_there", ["ngana", "balungkul", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; yinyay support only."),
    row(42, "We see the marku species of oyster there.", "Ngana marku yinyay nyajil.", "ngana marku yinyay nyaji-l | we species.of.oyster there see-NONPAST", "r172_present_we_marku_there", ["ngana", "marku", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; target bare."),
    row(43, "We see the mud oyster there.", "Ngana juwarru yinyay nyajil.", "ngana juwarru yinyay nyaji-l | we mud.oyster there see-NONPAST", "r172_present_we_juwarru_there", ["ngana", "juwarru", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; no habitat/comparison row."),
    row(44, "We see the spider shell there.", "Ngana marakal yinyay nyajil.", "ngana marakal yinyay nyaji-l | we spider.shell there see-NONPAST", "r172_present_we_marakal_there", ["ngana", "marakal", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; target bare."),
    row(45, "We see the marubu species of oyster there.", "Ngana marubu yinyay nyajil.", "ngana marubu yinyay nyaji-l | we species.of.oyster there see-NONPAST", "r172_present_we_marubu_there", ["ngana", "marubu", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; target bare."),
    row(46, "We see the big clam shell there.", "Ngana munji yinyay nyajil.", "ngana munji yinyay nyaji-l | we big.clam.shell there see-NONPAST", "r172_present_we_munji_there", ["ngana", "munji", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; no use row."),
    row(47, "We see the seashell there.", "Ngana ngulumuku yinyay nyajil.", "ngana ngulumuku yinyay nyaji-l | we seashell there see-NONPAST", "r172_present_we_ngulumuku_there", ["ngana", "ngulumuku", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; target bare."),
    row(48, "We see the freshwater prawn there.", "Ngana wukuju yinyay nyajil.", "ngana wukuju yinyay nyaji-l | we freshwater.prawn there see-NONPAST", "r172_present_we_wukuju_there", ["ngana", "wukuju", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; no size/leg row."),
    row(49, "We see the jellyfish there.", "Ngana wuybur yinyay nyajil.", "ngana wuybur yinyay nyaji-l | we jellyfish there see-NONPAST", "r172_present_we_wuybur_there", ["ngana", "wuybur", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; no sting/touch row."),
    row(50, "We see the large round oyster there.", "Ngana yulba yinyay nyajil.", "ngana yulba yinyay nyaji-l | we large.round.oyster there see-NONPAST", "r172_present_we_yulba_there", ["ngana", "yulba", "yinyay", "nyaji-l"], "Inclusive pronoun A bare; target bare."),
]


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


def first_word(low: str) -> str:
    return low.split()[0].strip(".,!?;:\"")


def has_target_case(low: str, head: str) -> bool:
    return re.search(rf"(?<![a-z-]){re.escape(head)}-(?: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)\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)}\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("r172_"):
            issues.append(f"{label}: Rule 172 frame must start with r172_")
        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 172")
        for denied in DENY_KUKU:
            if has_token(low, denied):
                issues.append(f"{label}: denied rejected/use 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 172 target head, found {targets}")
        for head in targets:
            selected = TARGET_HEADS[head]
            if head not in words:
                issues.append(f"{label}: target head {head} missing from words_used")
            if selected.replace(" ", ".") not in analysis and selected not in analysis:
                issues.append(f"{label}: target head {head} must be glossed as {selected}")
            if selected not in english_low:
                issues.append(f"{label}: {head} must be translated with selected sense {selected!r}")
            if has_target_case(low, head):
                issues.append(f"{label}: target head {head} has forbidden case/compound morphology")
        if seq <= 10:
            if "nyajil" in low or "nyajin" in low or "nyaji-l" in words:
                issues.append(f"{label}: deictic rows must not use nyaji-l")
            if not has_token(low, "yinyay") or "yinyay" not in words or "there" not in english_low:
                issues.append(f"{label}: Rule 172 deictic rows require yinyay/there")
        else:
            if ("nyajin" not in low and "nyajil" not in low) or "nyaji-l" not in words:
                issues.append(f"{label}: Rule 172 perception rows must use nyaji-l")
            first = first_word(low)
            if first not in PRONOUN_A:
                issues.append(f"{label}: perception A should be an approved bare pronoun")
            if 11 <= seq <= 20 and (first != "ngayu" or not has_token(low, "yaluy") or "here" not in english_low):
                issues.append(f"{label}: rows 11-20 require ngayu + yaluy/here")
            if 21 <= seq <= 30 and (first != "yundu" or not has_token(low, "yaluy") or "here" not in english_low):
                issues.append(f"{label}: rows 21-30 require yundu + yaluy/here")
            if 31 <= seq <= 40 and (first != "jana" or not has_token(low, "yaluy") or "here" not in english_low or "saw" not in english_low):
                issues.append(f"{label}: rows 31-40 require jana + yaluy/here + past saw")
            if 41 <= seq <= 50 and (first != "ngana" or not has_token(low, "yinyay") or "there" not in english_low):
                issues.append(f"{label}: rows 41-50 require ngana + yinyay/there")
            if "nyajin" in low and "see-PAST" not in analysis:
                issues.append(f"{label}: nyajin must be glossed as see-PAST")
            if "nyajil" in low and "see-NONPAST" not in analysis:
                issues.append(f"{label}: nyajil must be glossed as see-NONPAST")
        if any(english_has_denied(english_low, bad) for bad in DENY_ENGLISH):
            issues.append(f"{label}: denied use/event/habitat 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,))
        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 172 draft: construction consolidation after Rule 171", "operator-guide-2026-07-08", "under_review", NOW),
        )
        batch_id = cur.lastrowid

    dupes: list[str] = []
    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 172 source pass retained the Rule 171 shellfish reference heads balungkul, juwarru, marakal, marku, marubu, munji, ngulumuku, wukuju, wuybur, and yulba. Dictionary exact readback reconfirmed selected senses. Patz readback found no target-head conflicts and reconfirmed support forms yaluy/yinyay, nyaji-l/nyaji-ny, and bare pronoun A forms. Boundary-aware corpus readback found exactly the clean Batch 213 rows for retained targets.",
                 command="structured dictionary readback; rg grammar readback; sqlite boundary-aware corpus readback",
                 artifact_path=PROCESS_LOG, created_at=NOW)
    record_event(db, batch_label=LABEL, phase="scope", event_type="construction-scope", actor="claude-main",
                 detail="Rule 172 is construction consolidation over the Rule 171 shellfish-reference set. It uses bare target heads only and rotates yinyay/yaluy support across deictic and perception frames. It excludes jirrum, eating, edible status, bait, catching, gathering, cooking, cleaning, roasting, boiling, habitat, clinging, movement, body-part behavior, sting, touch, danger, preparation, use, target case, target compounds, possession, and event claims.",
                 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 found 50 planned Kuku surfaces, 50 unique within-batch surfaces, and no exact accepted/revised/under_review cross-corpus duplicates.",
                 command="sqlite exact lower(trim(kuku)) duplicate scan before banking", artifact_path="batch_scripts/bank_214.py", created_at=NOW)
    record_event(db, batch_label=LABEL, phase="preflight", event_type="static-guard-pass", actor="claude-main",
                 detail="Static guards passed: exactly one Rule 172 shellfish target per row; all targets bare; no loans; no rejected/use/event/habitat tokens; no target case/compound forms; yinyay deictic rows; yaluy-supported ngayu/yundu/jana perception rows; yinyay-supported ngana perception rows.",
                 command="python3 batch_scripts/bank_214.py static scan", artifact_path="batch_scripts/bank_214.py", created_at=NOW)

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

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


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