#!/usr/bin/env python3
"""Bank batch-280: Rule 238 dwelling and household reference 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-280"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-280-dwelling-household-reference-expansion-after-rule237-process.md"

TARGET_HEADS = {
    "bakarr": "ridge pole",
    "bayan": "house",
    "bira": "windbreak",
    "jiji": "roof",
    "kilili": "windbreak",
    "wurun": "humpy",
    "jakway": "door",
    "yulal": "board",
    "kararr": "blanket",
    "bilangkurr": "blanket",
}
SUPPORT = {"yinya", "yaluy", "yinyay", "nyulu", "nyajil"}
DENY_KUKU = {
    # Excluded homographs and adjacent source-prose traps.
    "yamba", "yamba-kari", "yamba-yamba", "jakal", "warrbi", "dubal", "kakarr",
    "karrngka", "dimbi", "wanjakan", "walal", "walan", "wadil", "ngaral",
    "ngaran", "yuru-dudal", "balanbalan", "balibali", "murba",
    # Build/use/action/source-material traps.
    "balkan", "buyku", "dumbul", "juku", "jikan", "kaji", "walngkan-daman",
    "walngkan", "nandan", "wunanka", "bundanday", "warngku", "wundi",
    # Demonstrative/pronoun case, perception compounds, and support inflection.
    "nyunguny", "yinyangka", "yinyayngka", "yinyayanka", "yinyamba",
    "yinyayanda", "yinyamuny", "yinyamundu", "yinyanji", "yinyarriny",
    "nyaji-nka", "nyaji-nyaku", "nyaji-l-nyaji",
    # Recent fire and body-part fields remain closed.
    "baya", "jimal", "jirramar", "kubu", "kunjin", "miyilkura", "ngalku",
    "ngaluri", "ngunjil", "wula", "bandin", "bari", "bibar", "birri",
    "jiba", "jandal", "jibar", "kangkul", "kuwinyu", "waba",
}
DENY_ENGLISH = (
    "build", "built", "building", "make", "made", "making", "set up", "camp out",
    "camping", "sleep", "sleeping", "slept", "lie on", "lying", "spread", "spreading",
    "cover", "covered", "covering", "warm", "warming", "cold", "rain", "storm",
    "bark", "grass", "sapling", "timber", "wood", "roofing", "front", "open",
    "opened", "close", "closed", "shut", "nail", "nailed", "carpenter", "plank",
    "round stick", "opium", "none", "nothing", "mouse", "axe", "old house",
    "place name", "wall", "crack", "floor", "inside", "outside", "own", "my ",
    "your ", "his ", "her ", "their ", "possess", "owner", "case", "compound",
    "loan", "ceremony", "spirit", "dead", "corpse", "purify", "smoke out",
)


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


EVIDENCE = {
    "bakarr": "Dictionary bakarr noun gloss 'ridge pole', definitions/translations 'ridge pole; pole', semantic_domain shelter-dwelling. Rule 238 uses ridge-pole reference/perception only. Patz 3.1.2 identifies nouns as terms for artifacts and objects of human habitat. Patz 3.7 sources yinya/yaluy/yinyay and noun-demonstrative order; Patz 3.5 and 3.8.3.2 source nyulu and nyaji-l 'see'.",
    "bayan": "Dictionary bayan noun gloss 'house', definitions 'house, camp, shelter', translations 'house; camp; shelter', semantic_domain shelter-dwelling; Patz 3.1.2 also lists bayan 'camp, house, shelter' as a generic noun. Rule 238 selects house reference/perception only, not camping, building, possession, or locative house prose. Patz 3.7 sources yinya/yaluy/yinyay and Patz 3.5/3.8.3.2 source nyulu nyajil.",
    "bira": "Dictionary bira noun gloss/definition/translation 'windbreak', semantic_domain shelter-dwelling. Rule 238 uses windbreak reference/perception only, with no making, setting, weather, or material-source claim. Patz 3.1.2, 3.7, 3.5, and 3.8.3.2 source the noun/reference/perception support.",
    "jiji": "Dictionary jiji noun gloss 'roof', definitions 'roof of house; the top of anything', translations 'roof; top', semantic_domain shelter-dwelling. Rule 238 selects roof-of-house reference/perception only and does not open the broad top-of-anything extension or roofing-material prose. Patz 3.7 and 3.5/3.8.3.2 source the support forms.",
    "kilili": "Dictionary kilili noun gloss/definition/translation 'windbreak', semantic_domain shelter-dwelling. Rule 238 uses windbreak reference/perception only, without making, setting, weather, or material-source claims. Patz 3.1.2, 3.7, 3.5, and 3.8.3.2 source the support forms.",
    "wurun": "Dictionary wurun noun gloss/definition 'humpy', translations 'humpy; halfway', semantic_domain shelter-dwelling. Rule 238 selects humpy reference/perception only, not building, sleeping, rain shelter, locative, or the 'halfway' translation. Patz 3.7 and 3.5/3.8.3.2 source the support forms.",
    "jakway": "Dictionary jakway noun gloss 'door', definitions/translations 'door; front', semantic_domain artefact. Rule 238 selects door reference/perception only and leaves front/opening/closing/shutting and jakal homograph material closed. Patz 3.7 and 3.5/3.8.3.2 source the support forms.",
    "yulal": "Dictionary yulal noun gloss 'board', definitions 'board, plank; contrast with kimil round stick', translations 'board; plank', semantic_domain artefact. Rule 238 selects board reference/perception only and does not open plank contrast, round-stick contrast, nailing, building, or source-material prose. Patz 3.7 and 3.5/3.8.3.2 source the support forms.",
    "kararr": "Grammar-added dictionary entry kararr noun gloss 'blanket', definition 'blanket, sheet, or grass bedding spread out to lie on', with Patz exx.153/416/563 and section 2.1.1. Rule 238 selects blanket reference/perception only, not spreading, sleeping, covering, grass bedding, or year-counting. Patz 3.7 and 3.5/3.8.3.2 source the support forms.",
    "bilangkurr": "Dictionary bilangkurr noun gloss/definition/translation 'blanket', semantic_domain clothing-adornment. Rule 238 uses blanket reference/perception only and does not open sleeping, covering, warming, clothing, or ownership prose. Patz 3.7 and 3.5/3.8.3.2 source the support forms.",
}


ROWS = [
    row(1, "That ridge pole is here.", "Yinya bakarr yaluy.", "yinya bakarr yaluy | that ridge.pole here", "r238_yinya_target_here_bakarr", ["yinya", "bakarr", "yaluy"], EVIDENCE["bakarr"]),
    row(2, "That house is here.", "Yinya bayan yaluy.", "yinya bayan yaluy | that house here", "r238_yinya_target_here_bayan", ["yinya", "bayan", "yaluy"], EVIDENCE["bayan"]),
    row(3, "That windbreak is here.", "Yinya bira yaluy.", "yinya bira yaluy | that windbreak here", "r238_yinya_target_here_bira", ["yinya", "bira", "yaluy"], EVIDENCE["bira"]),
    row(4, "That roof is here.", "Yinya jiji yaluy.", "yinya jiji yaluy | that roof here", "r238_yinya_target_here_jiji", ["yinya", "jiji", "yaluy"], EVIDENCE["jiji"]),
    row(5, "That windbreak is here.", "Yinya kilili yaluy.", "yinya kilili yaluy | that windbreak here", "r238_yinya_target_here_kilili", ["yinya", "kilili", "yaluy"], EVIDENCE["kilili"]),
    row(6, "That humpy is here.", "Yinya wurun yaluy.", "yinya wurun yaluy | that humpy here", "r238_yinya_target_here_wurun", ["yinya", "wurun", "yaluy"], EVIDENCE["wurun"]),
    row(7, "That door is here.", "Yinya jakway yaluy.", "yinya jakway yaluy | that door here", "r238_yinya_target_here_jakway", ["yinya", "jakway", "yaluy"], EVIDENCE["jakway"]),
    row(8, "That board is here.", "Yinya yulal yaluy.", "yinya yulal yaluy | that board here", "r238_yinya_target_here_yulal", ["yinya", "yulal", "yaluy"], EVIDENCE["yulal"]),
    row(9, "That blanket is here.", "Yinya kararr yaluy.", "yinya kararr yaluy | that blanket here", "r238_yinya_target_here_kararr", ["yinya", "kararr", "yaluy"], EVIDENCE["kararr"]),
    row(10, "That blanket is here.", "Yinya bilangkurr yaluy.", "yinya bilangkurr yaluy | that blanket here", "r238_yinya_target_here_bilangkurr", ["yinya", "bilangkurr", "yaluy"], EVIDENCE["bilangkurr"]),
    row(11, "That ridge pole is there.", "Yinya bakarr yinyay.", "yinya bakarr yinyay | that ridge.pole there", "r238_yinya_target_there_bakarr", ["yinya", "bakarr", "yinyay"], EVIDENCE["bakarr"]),
    row(12, "That house is there.", "Yinya bayan yinyay.", "yinya bayan yinyay | that house there", "r238_yinya_target_there_bayan", ["yinya", "bayan", "yinyay"], EVIDENCE["bayan"]),
    row(13, "That windbreak is there.", "Yinya bira yinyay.", "yinya bira yinyay | that windbreak there", "r238_yinya_target_there_bira", ["yinya", "bira", "yinyay"], EVIDENCE["bira"]),
    row(14, "That roof is there.", "Yinya jiji yinyay.", "yinya jiji yinyay | that roof there", "r238_yinya_target_there_jiji", ["yinya", "jiji", "yinyay"], EVIDENCE["jiji"]),
    row(15, "That windbreak is there.", "Yinya kilili yinyay.", "yinya kilili yinyay | that windbreak there", "r238_yinya_target_there_kilili", ["yinya", "kilili", "yinyay"], EVIDENCE["kilili"]),
    row(16, "That humpy is there.", "Yinya wurun yinyay.", "yinya wurun yinyay | that humpy there", "r238_yinya_target_there_wurun", ["yinya", "wurun", "yinyay"], EVIDENCE["wurun"]),
    row(17, "That door is there.", "Yinya jakway yinyay.", "yinya jakway yinyay | that door there", "r238_yinya_target_there_jakway", ["yinya", "jakway", "yinyay"], EVIDENCE["jakway"]),
    row(18, "That board is there.", "Yinya yulal yinyay.", "yinya yulal yinyay | that board there", "r238_yinya_target_there_yulal", ["yinya", "yulal", "yinyay"], EVIDENCE["yulal"]),
    row(19, "That blanket is there.", "Yinya kararr yinyay.", "yinya kararr yinyay | that blanket there", "r238_yinya_target_there_kararr", ["yinya", "kararr", "yinyay"], EVIDENCE["kararr"]),
    row(20, "That blanket is there.", "Yinya bilangkurr yinyay.", "yinya bilangkurr yinyay | that blanket there", "r238_yinya_target_there_bilangkurr", ["yinya", "bilangkurr", "yinyay"], EVIDENCE["bilangkurr"]),
    row(21, "That ridge pole is here.", "Bakarr yinya yaluy.", "bakarr yinya yaluy | ridge.pole that here", "r238_target_yinya_here_bakarr", ["bakarr", "yinya", "yaluy"], EVIDENCE["bakarr"]),
    row(22, "That house is here.", "Bayan yinya yaluy.", "bayan yinya yaluy | house that here", "r238_target_yinya_here_bayan", ["bayan", "yinya", "yaluy"], EVIDENCE["bayan"]),
    row(23, "That windbreak is here.", "Bira yinya yaluy.", "bira yinya yaluy | windbreak that here", "r238_target_yinya_here_bira", ["bira", "yinya", "yaluy"], EVIDENCE["bira"]),
    row(24, "That roof is here.", "Jiji yinya yaluy.", "jiji yinya yaluy | roof that here", "r238_target_yinya_here_jiji", ["jiji", "yinya", "yaluy"], EVIDENCE["jiji"]),
    row(25, "That windbreak is here.", "Kilili yinya yaluy.", "kilili yinya yaluy | windbreak that here", "r238_target_yinya_here_kilili", ["kilili", "yinya", "yaluy"], EVIDENCE["kilili"]),
    row(26, "That humpy is here.", "Wurun yinya yaluy.", "wurun yinya yaluy | humpy that here", "r238_target_yinya_here_wurun", ["wurun", "yinya", "yaluy"], EVIDENCE["wurun"]),
    row(27, "That door is here.", "Jakway yinya yaluy.", "jakway yinya yaluy | door that here", "r238_target_yinya_here_jakway", ["jakway", "yinya", "yaluy"], EVIDENCE["jakway"]),
    row(28, "That board is here.", "Yulal yinya yaluy.", "yulal yinya yaluy | board that here", "r238_target_yinya_here_yulal", ["yulal", "yinya", "yaluy"], EVIDENCE["yulal"]),
    row(29, "That blanket is here.", "Kararr yinya yaluy.", "kararr yinya yaluy | blanket that here", "r238_target_yinya_here_kararr", ["kararr", "yinya", "yaluy"], EVIDENCE["kararr"]),
    row(30, "That blanket is here.", "Bilangkurr yinya yaluy.", "bilangkurr yinya yaluy | blanket that here", "r238_target_yinya_here_bilangkurr", ["bilangkurr", "yinya", "yaluy"], EVIDENCE["bilangkurr"]),
    row(31, "That ridge pole is there.", "Bakarr yinya yinyay.", "bakarr yinya yinyay | ridge.pole that there", "r238_target_yinya_there_bakarr", ["bakarr", "yinya", "yinyay"], EVIDENCE["bakarr"]),
    row(32, "That house is there.", "Bayan yinya yinyay.", "bayan yinya yinyay | house that there", "r238_target_yinya_there_bayan", ["bayan", "yinya", "yinyay"], EVIDENCE["bayan"]),
    row(33, "That windbreak is there.", "Bira yinya yinyay.", "bira yinya yinyay | windbreak that there", "r238_target_yinya_there_bira", ["bira", "yinya", "yinyay"], EVIDENCE["bira"]),
    row(34, "That roof is there.", "Jiji yinya yinyay.", "jiji yinya yinyay | roof that there", "r238_target_yinya_there_jiji", ["jiji", "yinya", "yinyay"], EVIDENCE["jiji"]),
    row(35, "That windbreak is there.", "Kilili yinya yinyay.", "kilili yinya yinyay | windbreak that there", "r238_target_yinya_there_kilili", ["kilili", "yinya", "yinyay"], EVIDENCE["kilili"]),
    row(36, "That humpy is there.", "Wurun yinya yinyay.", "wurun yinya yinyay | humpy that there", "r238_target_yinya_there_wurun", ["wurun", "yinya", "yinyay"], EVIDENCE["wurun"]),
    row(37, "That door is there.", "Jakway yinya yinyay.", "jakway yinya yinyay | door that there", "r238_target_yinya_there_jakway", ["jakway", "yinya", "yinyay"], EVIDENCE["jakway"]),
    row(38, "That board is there.", "Yulal yinya yinyay.", "yulal yinya yinyay | board that there", "r238_target_yinya_there_yulal", ["yulal", "yinya", "yinyay"], EVIDENCE["yulal"]),
    row(39, "That blanket is there.", "Kararr yinya yinyay.", "kararr yinya yinyay | blanket that there", "r238_target_yinya_there_kararr", ["kararr", "yinya", "yinyay"], EVIDENCE["kararr"]),
    row(40, "That blanket is there.", "Bilangkurr yinya yinyay.", "bilangkurr yinya yinyay | blanket that there", "r238_target_yinya_there_bilangkurr", ["bilangkurr", "yinya", "yinyay"], EVIDENCE["bilangkurr"]),
    row(41, "He sees the ridge pole there.", "Nyulu bakarr yinyay nyajil.", "nyulu bakarr yinyay nyajil | 3sg.NOM ridge.pole there see.NPST", "r238_3sg_target_there_see_bakarr", ["nyulu", "bakarr", "yinyay", "nyajil"], EVIDENCE["bakarr"]),
    row(42, "He sees the house there.", "Nyulu bayan yinyay nyajil.", "nyulu bayan yinyay nyajil | 3sg.NOM house there see.NPST", "r238_3sg_target_there_see_bayan", ["nyulu", "bayan", "yinyay", "nyajil"], EVIDENCE["bayan"]),
    row(43, "He sees the windbreak there.", "Nyulu bira yinyay nyajil.", "nyulu bira yinyay nyajil | 3sg.NOM windbreak there see.NPST", "r238_3sg_target_there_see_bira", ["nyulu", "bira", "yinyay", "nyajil"], EVIDENCE["bira"]),
    row(44, "He sees the roof there.", "Nyulu jiji yinyay nyajil.", "nyulu jiji yinyay nyajil | 3sg.NOM roof there see.NPST", "r238_3sg_target_there_see_jiji", ["nyulu", "jiji", "yinyay", "nyajil"], EVIDENCE["jiji"]),
    row(45, "He sees the windbreak there.", "Nyulu kilili yinyay nyajil.", "nyulu kilili yinyay nyajil | 3sg.NOM windbreak there see.NPST", "r238_3sg_target_there_see_kilili", ["nyulu", "kilili", "yinyay", "nyajil"], EVIDENCE["kilili"]),
    row(46, "He sees the humpy there.", "Nyulu wurun yinyay nyajil.", "nyulu wurun yinyay nyajil | 3sg.NOM humpy there see.NPST", "r238_3sg_target_there_see_wurun", ["nyulu", "wurun", "yinyay", "nyajil"], EVIDENCE["wurun"]),
    row(47, "He sees the door there.", "Nyulu jakway yinyay nyajil.", "nyulu jakway yinyay nyajil | 3sg.NOM door there see.NPST", "r238_3sg_target_there_see_jakway", ["nyulu", "jakway", "yinyay", "nyajil"], EVIDENCE["jakway"]),
    row(48, "He sees the board there.", "Nyulu yulal yinyay nyajil.", "nyulu yulal yinyay nyajil | 3sg.NOM board there see.NPST", "r238_3sg_target_there_see_yulal", ["nyulu", "yulal", "yinyay", "nyajil"], EVIDENCE["yulal"]),
    row(49, "He sees the blanket there.", "Nyulu kararr yinyay nyajil.", "nyulu kararr yinyay nyajil | 3sg.NOM blanket there see.NPST", "r238_3sg_target_there_see_kararr", ["nyulu", "kararr", "yinyay", "nyajil"], EVIDENCE["kararr"]),
    row(50, "He sees the blanket there.", "Nyulu bilangkurr yinyay nyajil.", "nyulu bilangkurr yinyay nyajil | 3sg.NOM blanket there see.NPST", "r238_3sg_target_there_see_bilangkurr", ["nyulu", "bilangkurr", "yinyay", "nyajil"], EVIDENCE["bilangkurr"]),
]


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("r238_"):
            issues.append(f"{label}: Rule 238 frame must start with r238_")
        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 238")
        for denied in DENY_KUKU:
            if has_token(low, denied):
                issues.append(f"{label}: denied carry-forward 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 238 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 (not low.startswith("yinya ") or not low.endswith(" yaluy.") or len(low.split()) != 3):
            issues.append(f"{label}: rows 1-10 require Yinya TARGET yaluy")
        if 11 <= seq <= 20 and (not low.startswith("yinya ") or not low.endswith(" yinyay.") or len(low.split()) != 3):
            issues.append(f"{label}: rows 11-20 require Yinya TARGET yinyay")
        if 21 <= seq <= 30 and (not low.endswith(" yinya yaluy.") or low.startswith("yinya ") or len(low.split()) != 3):
            issues.append(f"{label}: rows 21-30 require TARGET yinya yaluy")
        if 31 <= seq <= 40 and (not low.endswith(" yinya yinyay.") or low.startswith("yinya ") or len(low.split()) != 3):
            issues.append(f"{label}: rows 31-40 require TARGET yinya yinyay")
        if 41 <= seq <= 50 and (not low.startswith("nyulu ") or not low.endswith(" yinyay nyajil.") or len(low.split()) != 4):
            issues.append(f"{label}: rows 41-50 require Nyulu TARGET yinyay nyajil")
        if any(english_has_denied(english_low, bad) for bad in DENY_ENGLISH):
            issues.append(f"{label}: denied dwelling action/use/homograph/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 lessons 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 238 draft", "operator-guide-2026-07-09", "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}: {kuku} -> {found}")
    if collisions:
        raise SystemExit("collision scan failed:\n" + "\n".join(collisions))

    db.executemany(
        """
        INSERT INTO sentences(
          batch_id, seq, english, kuku, analysis, frame, tier, words_used,
          loans_used, evidence, status, confidence, created_at
        )
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        [
            (
                batch_id,
                seq,
                english,
                kuku,
                analysis,
                frame,
                tier,
                words_used,
                loans_used,
                evidence,
                "under_review",
                confidence,
                NOW,
            )
            for seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, confidence in ROWS
        ],
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="source-readback",
        actor="claude-main",
        detail="Rule 238 source pass checked dictionary/grammar entries for bakarr ridge pole, bayan house/camp/shelter, bira windbreak, jiji roof/top, kilili windbreak, wurun humpy, jakway door/front, yulal board/plank, kararr blanket/sheet/grass bedding, and bilangkurr blanket. Patz 3.1.2 supports nouns for artifacts and objects of human habitat; Patz 3.7 supports yinya/yaluy/yinyay and noun-demonstrative order; Patz 3.5 and 3.8.3.2 support nyulu and nyaji-l.",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="planning",
        event_type="lexical-scope",
        actor="claude-main",
        detail="Selected Rule 238 dwelling and household reference expansion after Rule 237: ten noun heads in preposed-demonstrative, postposed-demonstrative, and one 3sg perception band. Excluded yamba, jakal, warrbi, dubal, kakarr, karrngka, dimbi, wanjakan, build/spread/enter/nail verbs, and all house-use/source/action prose.",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="coverage-audit",
        actor="claude-main",
        detail="Pre-batch token-boundary coverage among accepted/revised rows: bakarr 0, bayan 437, bira 1, jiji 0, kilili 0, wurun 23, jakway 10, yulal 10, kararr 17, and bilangkurr 17.",
        command="sqlite3 synthetic.db WITH targets(t) AS (...) SELECT t, COUNT(*) ...",
        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 238 Kuku surfaces returned zero accepted, revised, or under-review collisions.",
        command="python3 batch_scripts/bank_280.py internal collision scan",
        output_excerpt="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="Static guards passed: 50 rows, exactly one Rule 238 target noun per row, no loans, no target suffixes, no excluded homograph/action/source-prose target drift, no pronoun case, no perception compound, and five planned frame bands only.",
        command="python3 batch_scripts/bank_280.py",
        artifact_path="batch_scripts/bank_280.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="bank-success",
        actor="claude-main",
        detail="Banked 50 hand-authored Rule 238 dwelling and household reference rows as under_review.",
        command="python3 batch_scripts/bank_280.py",
        artifact_path="batch_scripts/bank_280.py",
        created_at=NOW,
    )
    db.commit()
    print(f"banked {LABEL}: 50 under_review rows")
    return 0


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