#!/usr/bin/env python3
"""Bank batch-196: Rule 154 construction consolidation after Rule 153."""

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

TARGET_HEADS = {
    "kaba": "rain",
    "muyar": "wind",
    "ngurrban": "cloud",
    "wungar": "sun",
    "kija": "moon",
    "yiril": "rainbow",
    "jarramali": "thunderstorm",
    "jabulbina": "thunder",
    "kurrubijal": "morning.star",
    "wurunurru": "whirlwind",
}
TARGET_ENGLISH = {
    "kaba": "rain",
    "muyar": "wind",
    "ngurrban": "cloud",
    "wungar": "sun",
    "kija": "moon",
    "yiril": "rainbow",
    "jarramali": "thunderstorm",
    "jabulbina": "thunder",
    "kurrubijal": "morning star",
    "wurunurru": "whirlwind",
}
PRONOUN_A = {"ngayu", "yundu", "jana"}
COMMON_A = {"ngamu-ngku"}
FORBIDDEN_HEADS = {"wuburr", "balay", "dubar", "kaba-kada", "jarunga-kija", "juru-kija", "jurungu-kija"}
DENY_KUKU = {
    "dama", "daman", "damanka", "nyurrba", "nyurrban", "yaka", "yakan", "kuni", "kunin", "baka", "bakan",
    "wundi", "wundin", "bangka", "bangkan", "balka", "balkan", "kaja", "kajan", "yurril", "yurrin",
    "kida", "kidan", "jurrnga", "jurrngan", "yilba", "yilban", "duda", "dudan", "nyanda", "nyandan",
    "mani", "manin", "yijarri", "yijarrin", "daka", "dakan", "waju", "wajun", "wunba", "mayi",
    "dunga", "dungan", "dungay", "kadan", "kaday", "kada", "janji", "janjil", "mariji", "marijin",
    "dara", "daran", "daray", "bunjurril", "wandil", "wandin", "ngaki", "wumbul", "wungariji",
}
DENY_ENGLISH = (
    "fall", "fell", "falling", "come", "came", "coming", "blow", "blows", "blew", "storm came",
    "sunrise", "sunset", "season", "month", "roaring meg", "place name", "rainbird", "rain forest",
    "stingray", "praying mantis", "angry", "hot-tempered", "drive away", "omen", "hot season",
)


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 rain is there.", "Kaba yinyay.", "kaba yinyay | rain there", "r154_deictic_rain_there", ["kaba", "yinyay"], "Dictionary kaba rain; Patz demonstrative yinyay there; bare deictic identification."),
    row(2, "The wind is there.", "Muyar yinyay.", "muyar yinyay | wind there", "r154_deictic_wind_there", ["muyar", "yinyay"], "Dictionary muyar wind; Patz demonstrative yinyay there."),
    row(3, "The cloud is there.", "Ngurrban yinyay.", "ngurrban yinyay | cloud there", "r154_deictic_cloud_there", ["ngurrban", "yinyay"], "Dictionary ngurrban cloud; yinyay support."),
    row(4, "The sun is there.", "Wungar yinyay.", "wungar yinyay | sun there", "r154_deictic_sun_there", ["wungar", "yinyay"], "Dictionary wungar sun; no day/season reading."),
    row(5, "The moon is there.", "Kija yinyay.", "kija yinyay | moon there", "r154_deictic_moon_there", ["kija", "yinyay"], "Dictionary/Patz kija moon; selected moon only."),
    row(6, "The rainbow is here.", "Yiril yaluy.", "yiril yaluy | rainbow here", "r154_deictic_rainbow_here", ["yiril", "yaluy"], "Dictionary yiril rainbow; Patz demonstrative yaluy here; avoids old duplicate Yiril yinyay."),
    row(7, "The thunderstorm is there.", "Jarramali yinyay.", "jarramali yinyay | thunderstorm there", "r154_deictic_thunderstorm_there", ["jarramali", "yinyay"], "Dictionary/Patz jarramali thunderstorm; selected weather sense only."),
    row(8, "The thunder is there.", "Jabulbina yinyay.", "jabulbina yinyay | thunder there", "r154_deictic_thunder_there", ["jabulbina", "yinyay"], "Dictionary jabulbina thunder; selected thunder only."),
    row(9, "The morning star is there.", "Kurrubijal yinyay.", "kurrubijal yinyay | morning.star there", "r154_deictic_morning_star_there", ["kurrubijal", "yinyay"], "Dictionary kurrubijal morning star."),
    row(10, "The whirlwind is there.", "Wurunurru yinyay.", "wurunurru yinyay | whirlwind there", "r154_deictic_whirlwind_there", ["wurunurru", "yinyay"], "Dictionary wurunurru whirlwind."),
    row(11, "I see the rain here.", "Ngayu kaba yaluy nyajil.", "ngayu kaba yaluy nyaji-l | I rain here see-NONPAST", "r154_present_i_rain_here", ["ngayu", "kaba", "yaluy", "nyaji-l"], "Pronoun A bare; yaluy local support; target bare."),
    row(12, "I hear the wind here.", "Ngayu muyar yaluy nyajil.", "ngayu muyar yaluy nyaji-l | I wind here hear-NONPAST", "r154_present_i_wind_here", ["ngayu", "muyar", "yaluy", "nyaji-l"], "nyaji-l covers hearing; wind target bare."),
    row(13, "I see the cloud here.", "Ngayu ngurrban yaluy nyajil.", "ngayu ngurrban yaluy nyaji-l | I cloud here see-NONPAST", "r154_present_i_cloud_here", ["ngayu", "ngurrban", "yaluy", "nyaji-l"], "Pronoun A bare; cloud target bare."),
    row(14, "I see the sun here.", "Ngayu wungar yaluy nyajil.", "ngayu wungar yaluy nyaji-l | I sun here see-NONPAST", "r154_present_i_sun_here", ["ngayu", "wungar", "yaluy", "nyaji-l"], "Sun object only; no hot-season reading."),
    row(15, "I see the moon here.", "Ngayu kija yaluy nyajil.", "ngayu kija yaluy nyaji-l | I moon here see-NONPAST", "r154_present_i_moon_here", ["ngayu", "kija", "yaluy", "nyaji-l"], "Moon object only; no month/place reading."),
    row(16, "I see the rainbow here.", "Ngayu yiril yaluy nyajil.", "ngayu yiril yaluy nyaji-l | I rainbow here see-NONPAST", "r154_present_i_rainbow_here", ["ngayu", "yiril", "yaluy", "nyaji-l"], "Rainbow object only."),
    row(17, "I hear the thunderstorm here.", "Ngayu jarramali yaluy nyajil.", "ngayu jarramali yaluy nyaji-l | I thunderstorm here hear-NONPAST", "r154_present_i_thunderstorm_here", ["ngayu", "jarramali", "yaluy", "nyaji-l"], "Thunderstorm weather sense only."),
    row(18, "I hear the thunder here.", "Ngayu jabulbina yaluy nyajil.", "ngayu jabulbina yaluy nyaji-l | I thunder here hear-NONPAST", "r154_present_i_thunder_here", ["ngayu", "jabulbina", "yaluy", "nyaji-l"], "Thunder object only."),
    row(19, "I see the morning star here.", "Ngayu kurrubijal yaluy nyajil.", "ngayu kurrubijal yaluy nyaji-l | I morning.star here see-NONPAST", "r154_present_i_morning_star_here", ["ngayu", "kurrubijal", "yaluy", "nyaji-l"], "Morning-star object only."),
    row(20, "I see the whirlwind here.", "Ngayu wurunurru yaluy nyajil.", "ngayu wurunurru yaluy nyaji-l | I whirlwind here see-NONPAST", "r154_present_i_whirlwind_here", ["ngayu", "wurunurru", "yaluy", "nyaji-l"], "Whirlwind object only."),
    row(21, "You see the rain here.", "Yundu kaba yaluy nyajil.", "yundu kaba yaluy nyaji-l | you rain here see-NONPAST", "r154_present_you_rain_here", ["yundu", "kaba", "yaluy", "nyaji-l"], "Patz yundu with nyaji-l/nyaji-ny; target bare."),
    row(22, "You hear the wind here.", "Yundu muyar yaluy nyajil.", "yundu muyar yaluy nyaji-l | you wind here hear-NONPAST", "r154_present_you_wind_here", ["yundu", "muyar", "yaluy", "nyaji-l"], "Second-person pronoun A bare; wind heard."),
    row(23, "You see the cloud here.", "Yundu ngurrban yaluy nyajil.", "yundu ngurrban yaluy nyaji-l | you cloud here see-NONPAST", "r154_present_you_cloud_here", ["yundu", "ngurrban", "yaluy", "nyaji-l"], "Second-person pronoun A bare; cloud object."),
    row(24, "You see the sun here.", "Yundu wungar yaluy nyajil.", "yundu wungar yaluy nyaji-l | you sun here see-NONPAST", "r154_present_you_sun_here", ["yundu", "wungar", "yaluy", "nyaji-l"], "Sun object only."),
    row(25, "You see the moon here.", "Yundu kija yaluy nyajil.", "yundu kija yaluy nyaji-l | you moon here see-NONPAST", "r154_present_you_moon_here", ["yundu", "kija", "yaluy", "nyaji-l"], "Moon object only."),
    row(26, "You see the rainbow here.", "Yundu yiril yaluy nyajil.", "yundu yiril yaluy nyaji-l | you rainbow here see-NONPAST", "r154_present_you_rainbow_here", ["yundu", "yiril", "yaluy", "nyaji-l"], "Rainbow object only."),
    row(27, "You hear the thunderstorm here.", "Yundu jarramali yaluy nyajil.", "yundu jarramali yaluy nyaji-l | you thunderstorm here hear-NONPAST", "r154_present_you_thunderstorm_here", ["yundu", "jarramali", "yaluy", "nyaji-l"], "Thunderstorm weather sense only."),
    row(28, "You hear the thunder here.", "Yundu jabulbina yaluy nyajil.", "yundu jabulbina yaluy nyaji-l | you thunder here hear-NONPAST", "r154_present_you_thunder_here", ["yundu", "jabulbina", "yaluy", "nyaji-l"], "Thunder object only."),
    row(29, "You see the morning star here.", "Yundu kurrubijal yaluy nyajil.", "yundu kurrubijal yaluy nyaji-l | you morning.star here see-NONPAST", "r154_present_you_morning_star_here", ["yundu", "kurrubijal", "yaluy", "nyaji-l"], "Morning-star object only."),
    row(30, "You see the whirlwind here.", "Yundu wurunurru yaluy nyajil.", "yundu wurunurru yaluy nyaji-l | you whirlwind here see-NONPAST", "r154_present_you_whirlwind_here", ["yundu", "wurunurru", "yaluy", "nyaji-l"], "Whirlwind object only."),
    row(31, "They saw the rain here.", "Jana kaba yaluy nyajin.", "jana kaba yaluy nyaji-ny | they rain here see-PAST", "r154_past_they_rain_here", ["jana", "kaba", "yaluy", "nyaji-l"], "Third-person plural pronoun A bare; past perception."),
    row(32, "They heard the wind here.", "Jana muyar yaluy nyajin.", "jana muyar yaluy nyaji-ny | they wind here hear-PAST", "r154_past_they_wind_here", ["jana", "muyar", "yaluy", "nyaji-l"], "Wind heard with nyaji-ny."),
    row(33, "They saw the cloud here.", "Jana ngurrban yaluy nyajin.", "jana ngurrban yaluy nyaji-ny | they cloud here see-PAST", "r154_past_they_cloud_here", ["jana", "ngurrban", "yaluy", "nyaji-l"], "Cloud object; target bare."),
    row(34, "They saw the sun here.", "Jana wungar yaluy nyajin.", "jana wungar yaluy nyaji-ny | they sun here see-PAST", "r154_past_they_sun_here", ["jana", "wungar", "yaluy", "nyaji-l"], "Sun object only."),
    row(35, "They saw the moon here.", "Jana kija yaluy nyajin.", "jana kija yaluy nyaji-ny | they moon here see-PAST", "r154_past_they_moon_here", ["jana", "kija", "yaluy", "nyaji-l"], "Moon object only."),
    row(36, "They saw the rainbow here.", "Jana yiril yaluy nyajin.", "jana yiril yaluy nyaji-ny | they rainbow here see-PAST", "r154_past_they_rainbow_here", ["jana", "yiril", "yaluy", "nyaji-l"], "Rainbow object only."),
    row(37, "They heard the thunderstorm here.", "Jana jarramali yaluy nyajin.", "jana jarramali yaluy nyaji-ny | they thunderstorm here hear-PAST", "r154_past_they_thunderstorm_here", ["jana", "jarramali", "yaluy", "nyaji-l"], "Thunderstorm weather sense only."),
    row(38, "They heard the thunder here.", "Jana jabulbina yaluy nyajin.", "jana jabulbina yaluy nyaji-ny | they thunder here hear-PAST", "r154_past_they_thunder_here", ["jana", "jabulbina", "yaluy", "nyaji-l"], "Thunder object only."),
    row(39, "They saw the morning star here.", "Jana kurrubijal yaluy nyajin.", "jana kurrubijal yaluy nyaji-ny | they morning.star here see-PAST", "r154_past_they_morning_star_here", ["jana", "kurrubijal", "yaluy", "nyaji-l"], "Morning-star object only."),
    row(40, "They saw the whirlwind here.", "Jana wurunurru yaluy nyajin.", "jana wurunurru yaluy nyaji-ny | they whirlwind here see-PAST", "r154_past_they_whirlwind_here", ["jana", "wurunurru", "yaluy", "nyaji-l"], "Whirlwind object only."),
    row(41, "Mother sees the rain here.", "Ngamu-ngku kaba yaluy nyajil.", "ngamu-ngku kaba yaluy nyaji-l | mother-ERG rain here see-NONPAST", "r154_present_mother_rain_here", ["ngamu", "kaba", "yaluy", "nyaji-l"], "Common-noun A ERG; rain object."),
    row(42, "Mother hears the wind here.", "Ngamu-ngku muyar yaluy nyajil.", "ngamu-ngku muyar yaluy nyaji-l | mother-ERG wind here hear-NONPAST", "r154_present_mother_wind_here", ["ngamu", "muyar", "yaluy", "nyaji-l"], "Common-noun A ERG; wind heard."),
    row(43, "Mother sees the cloud here.", "Ngamu-ngku ngurrban yaluy nyajil.", "ngamu-ngku ngurrban yaluy nyaji-l | mother-ERG cloud here see-NONPAST", "r154_present_mother_cloud_here", ["ngamu", "ngurrban", "yaluy", "nyaji-l"], "Common-noun A ERG; cloud object."),
    row(44, "Mother sees the sun here.", "Ngamu-ngku wungar yaluy nyajil.", "ngamu-ngku wungar yaluy nyaji-l | mother-ERG sun here see-NONPAST", "r154_present_mother_sun_here", ["ngamu", "wungar", "yaluy", "nyaji-l"], "Common-noun A ERG; sun object."),
    row(45, "Mother sees the moon here.", "Ngamu-ngku kija yaluy nyajil.", "ngamu-ngku kija yaluy nyaji-l | mother-ERG moon here see-NONPAST", "r154_present_mother_moon_here", ["ngamu", "kija", "yaluy", "nyaji-l"], "Common-noun A ERG; moon object."),
    row(46, "Mother sees the rainbow here.", "Ngamu-ngku yiril yaluy nyajil.", "ngamu-ngku yiril yaluy nyaji-l | mother-ERG rainbow here see-NONPAST", "r154_present_mother_rainbow_here", ["ngamu", "yiril", "yaluy", "nyaji-l"], "Common-noun A ERG; rainbow object."),
    row(47, "Mother hears the thunderstorm here.", "Ngamu-ngku jarramali yaluy nyajil.", "ngamu-ngku jarramali yaluy nyaji-l | mother-ERG thunderstorm here hear-NONPAST", "r154_present_mother_thunderstorm_here", ["ngamu", "jarramali", "yaluy", "nyaji-l"], "Common-noun A ERG; thunderstorm weather sense."),
    row(48, "Mother hears the thunder here.", "Ngamu-ngku jabulbina yaluy nyajil.", "ngamu-ngku jabulbina yaluy nyaji-l | mother-ERG thunder here hear-NONPAST", "r154_present_mother_thunder_here", ["ngamu", "jabulbina", "yaluy", "nyaji-l"], "Common-noun A ERG; thunder object."),
    row(49, "Mother sees the morning star here.", "Ngamu-ngku kurrubijal yaluy nyajil.", "ngamu-ngku kurrubijal yaluy nyaji-l | mother-ERG morning.star here see-NONPAST", "r154_present_mother_morning_star_here", ["ngamu", "kurrubijal", "yaluy", "nyaji-l"], "Common-noun A ERG; morning-star object."),
    row(50, "Mother sees the whirlwind here.", "Ngamu-ngku wurunurru yaluy nyajil.", "ngamu-ngku wurunurru yaluy nyaji-l | mother-ERG whirlwind here see-NONPAST", "r154_present_mother_whirlwind_here", ["ngamu", "wurunurru", "yaluy", "nyaji-l"], "Common-noun A ERG; whirlwind object."),
]


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|muny|darr|burr|bu|ka|nka|nku|ji|jirr|ku|mundu|amu|mu|iji)\b", low) is not None


def english_has_denied(english_low: str, denied: str) -> bool:
    if " " in denied:
        return denied in english_low
    return 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)
        is_deictic = seq <= 10
        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("r154_"):
            issues.append(f"{label}: Rule 154 frame must start with r154_")
        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 new loans are admitted in Rule 154")
        for denied in DENY_KUKU:
            if has_token(low, denied):
                issues.append(f"{label}: denied weather-event/action token {denied!r}")
        for denied in FORBIDDEN_HEADS:
            if has_token(low, denied):
                issues.append(f"{label}: forbidden place-name/old-field trap {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 retained Rule 153 target head, found {targets}")
        for head in targets:
            if head not in words:
                issues.append(f"{label}: target head {head} missing from words_used")
            if TARGET_HEADS[head] not in analysis:
                issues.append(f"{label}: target head {head} must be glossed as {TARGET_HEADS[head]}")
            if TARGET_ENGLISH[head] not in english_low:
                issues.append(f"{label}: {head} must be translated with selected sense {TARGET_ENGLISH[head]!r}")
            if has_target_case(low, head):
                issues.append(f"{label}: target head {head} has forbidden case/compound morphology")
        if has_token(low, "kija") and "moon" not in english_low:
            issues.append(f"{label}: kija must be translated as moon only in Rule 154")
        if has_token(low, "jarramali") and "thunderstorm" not in english_low:
            issues.append(f"{label}: jarramali must be translated as thunderstorm in Rule 154")
        if has_token(low, "jabulbina") and "thunder" not in english_low:
            issues.append(f"{label}: jabulbina must be translated as thunder in Rule 154")
        if is_deictic:
            if "nyajil" in low or "nyajin" in low or "nyaji-l" in words:
                issues.append(f"{label}: deictic identification rows must not use nyaji-l")
            if not (has_token(low, "yinyay") or has_token(low, "yaluy")):
                issues.append(f"{label}: deictic identification rows require yinyay or yaluy")
        else:
            if not has_token(low, "yaluy") or "yaluy" not in words:
                issues.append(f"{label}: Rule 154 perception rows require yaluy support")
            if ("nyajin" not in low and "nyajil" not in low) or "nyaji-l" not in words:
                issues.append(f"{label}: Rule 154 perception rows must use nyaji-l")
            first = first_word(low)
            if first not in PRONOUN_A and first not in COMMON_A:
                issues.append(f"{label}: perception A should be an approved bare pronoun or ERG-marked common noun")
            if first in COMMON_A and "-ngku" not in first:
                issues.append(f"{label}: common-noun A must be ERG-marked")
            if "nyajin" in low and not ("see-PAST" in analysis or "hear-PAST" in analysis):
                issues.append(f"{label}: nyajin must be glossed as see/hear-PAST")
            if "nyajil" in low and not ("see-NONPAST" in analysis or "hear-NONPAST" in analysis):
                issues.append(f"{label}: nyajil must be glossed as see/hear-NONPAST")
        if any(english_has_denied(english_low, bad) for bad in DENY_ENGLISH):
            issues.append(f"{label}: denied weather-event/polysemy 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 154 draft: construction consolidation after Rule 153", "operator-guide-2026-07-07", "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 154 source pass retained the Rule 153 weather/sky targets and added no new target heads. "
            "Dictionary, Patz grammar, lexeme-table, and corpus-surface readback confirmed yinyay/yaluy deictics, "
            "yundu/ngayu/jana/ngamu participants, and nyaji-l perception. Old Yiril yinyay duplicate was replaced "
            "with Yiril yaluy before banking."
        ),
        command="dictionary.yaml readback; grammar.md Table 3.12/3.14 and nyaji examples; sqlite lexeme and surface 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 154 is a construction consolidation pass: bare deictic identification plus yaluy-supported "
            "perception. It excludes all weather-event predicates, target case/compound morphology, season/time "
            "expressions, place-name drift, kija month/place senses, jarramali animal/temper senses, and carry-forward "
            "geography/implement traps."
        ),
        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 revised 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_196.py", created_at=NOW)
    record_event(db, batch_label=LABEL, phase="preflight", event_type="static-guard-pass", actor="claude-main",
                 detail="Static guards passed: one retained weather/sky target per row, selected English senses enforced, yinyay/yaluy support checked, no target case or compound morphology, no weather-event predicates, no place-name/animal/polysemy drift, common-noun A ERG, and pronoun A bare.",
                 command="python3 batch_scripts/bank_196.py static scan", artifact_path="batch_scripts/bank_196.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, seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, "under_review", confidence, "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 under_review Rule 154 construction-consolidation rows.",
                 command="python3 batch_scripts/bank_196.py", artifact_path="batch_scripts/bank_196.py", created_at=NOW)
    db.commit()

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


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