#!/usr/bin/env python3
"""Bank batch-171: Rule 129 material/landscape lexical expansion after Rule 128."""

from __future__ import annotations

import datetime as dt
import json
import re
import sqlite3
import sys

from process_ledger import ensure_schema, record_event

DB = "/mnt/donto-data/donto-resources/research/translation-training/kuku-yalanji-runpod-2026-06-30/synthetic/claude-synthetic-v1-2026-07-02/synthetic.db"
LABEL = "batch-171"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-171-material-landscape-lexical-expansion-after-rule128-process.md"

DENY = {"yah", "ngawuya", "walnga", "maja-nga", "yunga", "bada-bada", "wangkar-wangkar", "biyul", "warrur"}
CONSTRUCTION_CLITIC_MARKERS = ("-kuda", "-ngarr", "-ngurr", "-l-urr", "wawu-")
PRONOUN_A = {"ngayu", "ngana", "nganjin", "yundu", "yurra", "jana", "nyulu"}
SOURCED_COMITATIVES = ("ngawa-nji", "nganjan-anji", "jawun-unji", "juku-ji", "kambi-ji")
LAUGH_TARGET_SUFFIXES = ("-anka", "-anda", "-nda", "-ndu", "-nka")
LOAN_REQUIRED = ("mayngku", "wulman")


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


ROWS = [
    (1, "Leaves lay in the shade.",
     "Birra wabarr-ba wunan.",
     "birra wabarr-ba wuna-ny | leaf shade-LOC lie-PAST",
     "r129_rest_leaf_shade", 3, js(["birra", "wabarr", "wunay"]), js([]),
     "Dictionary birra leaf; Patz birra-jirr-ka tree-with-leaves; retained wabarr-ba/wuna-y frame.", "0.84"),
    (2, "Bark lay on the ground.",
     "Dumbul bubu-ngu wunan.",
     "dumbul bubu-ngu wuna-ny | bark ground-LOC lie-PAST",
     "r129_rest_bark_ground", 3, js(["dumbul", "bubu", "wunay"]), js([]),
     "Dictionary dumbul bark/hard covering; retained bubu-ngu/wuna-y rest frame.", "0.84"),
    (3, "Smoke came from camp.",
     "Kubu bayan-muny kadan.",
     "kubu bayan-muny kada-ny | smoke camp-ABL come-PAST",
     "r129_smoke_camp_source", 3, js(["kubu", "bayan", "kaday"]), js([]),
     "Dictionary kubu smoke; grammar smoke-seeing line; retained bayan-muny/kada-y source frame.", "0.84"),
    (4, "The stone lay in the shade.",
     "Kulji wabarr-ba wunan.",
     "kulji wabarr-ba wuna-ny | stone shade-LOC lie-PAST",
     "r129_rest_stone_shade", 3, js(["kulji", "wabarr", "wunay"]), js([]),
     "Patz kulji stone/rock but not mountain; retained wabarr-ba/wuna-y frame.", "0.84"),
    (5, "Sand lay in the bag.",
     "Jukar balji-nga wunan.",
     "jukar balji-nga wuna-ny | sand bag-LOC lie-PAST",
     "r129_rest_sand_bag", 3, js(["jukar", "balji", "wunay"]), js([]),
     "Dictionary jukar sand; retained balji-nga/wuna-y frame.", "0.84"),
    (6, "Seeds lay on the ground.",
     "Diburr bubu-ngu wunan.",
     "diburr bubu-ngu wuna-ny | seed ground-LOC lie-PAST",
     "r129_rest_seed_ground", 3, js(["diburr", "bubu", "wunay"]), js([]),
     "Dictionary diburr seed/egg; seed sense selected; retained bubu-ngu/wuna-y frame.", "0.84"),
    (7, "The flower is on the tree.",
     "Nganka juku-ngu janay.",
     "nganka juku-ngu jana-y | flower tree-LOC stand/be-NONPAST",
     "r129_flower_tree_stand", 3, js(["nganka", "juku", "janay"]), js([]),
     "Dictionary nganka flower; grammar nganka examples; retained juku-ngu/jana-y be frame.", "0.84"),
    (8, "Fog lay outside.",
     "Ngurrbal ngalkal-ba wunan.",
     "ngurrbal ngalkal-ba wuna-ny | fog outside-LOC lie-PAST",
     "r129_rest_fog_outside", 3, js(["ngurrbal", "ngalkal", "wunay"]), js([]),
     "Dictionary ngurrbal fog and prior heavy-fog corpus row; retained ngalkal-ba/wuna-y frame.", "0.84"),
    (9, "The young man went to the mountain.",
     "Warru manjal-ba dungan.",
     "warru manjal-ba dunga-ny | young.man mountain-LOC go-PAST",
     "r129_motion_young_man_mountain", 3, js(["warru", "manjal", "dungay"]), js([]),
     "Dictionary manjal mountain; grammar manjal-ba; retained dunga-y motion frame.", "0.84"),
    (10, "Mother went to the scrub.",
     "Ngamu madja-nga dungan.",
     "ngamu madja-nga dunga-ny | mother scrub-LOC go-PAST",
     "r129_motion_mother_scrub", 3, js(["ngamu", "madja", "dungay"]), js([]),
     "Dictionary madja scrub/rainforest; retained madja-nga/dunga-y frame.", "0.84"),

    (11, "The woman put leaves in the bag.",
     "Jalbu-ngku birra balji-nga yijarrin.",
     "jalbu-ngku birra balji-nga yijarri-ny | woman-ERG leaf bag-LOC put-PAST",
     "r129_place_leaf_bag", 4, js(["jalbu", "birra", "balji", "yijarril"]), js([]),
     "Birra leaf object in retained placement frame; common-noun A ERG.", "0.84"),
    (12, "The man put bark on the ground.",
     "Dingkar-angka dumbul bubu-ngu yijarrin.",
     "dingkar-angka dumbul bubu-ngu yijarri-ny | man-ERG bark ground-LOC put-PAST",
     "r129_place_bark_ground", 4, js(["dingkar", "dumbul", "bubu", "yijarril"]), js([]),
     "Dumbul bark object in retained placement frame; common-noun A ERG.", "0.84"),
    (13, "Grandmother left the stone on the road.",
     "Babi-ngka kulji baral-ba bawan.",
     "babi-ngka kulji baral-ba bawa-ny | grandmother-ERG stone road-LOC leave-PAST",
     "r129_leave_stone_road", 4, js(["babi", "kulji", "baral", "bawal"]), js([]),
     "Kulji stone object in retained leave-location frame; baral-ba road LOC.", "0.84"),
    (14, "Mother put sand in the bag.",
     "Ngamu-ngku jukar balji-nga yijarrin.",
     "ngamu-ngku jukar balji-nga yijarri-ny | mother-ERG sand bag-LOC put-PAST",
     "r129_place_sand_bag", 4, js(["ngamu", "jukar", "balji", "yijarril"]), js([]),
     "Jukar sand object in retained placement frame.", "0.84"),
    (15, "The young man took seeds to camp.",
     "Warru-ngku diburr bayan-ka wundin.",
     "warru-ngku diburr bayan-ka wundi-ny | young.man-ERG seed camp-DAT take-PAST",
     "r129_transfer_seed_camp_goal", 4, js(["warru", "diburr", "bayan", "wundil"]), js([]),
     "Diburr seed sense selected; retained bayan-ka transfer goal.", "0.84"),
    (16, "Grandfather brought leaves from the house.",
     "Kami-ngka birra bayan-muny wundin.",
     "kami-ngka birra bayan-muny wundi-ny | grandfather-ERG leaf house-ABL bring-PAST",
     "r129_transfer_leaf_house_source", 4, js(["kami", "birra", "bayan", "wundil"]), js([]),
     "Birra leaf object plus retained bayan-muny source.", "0.84"),
    (17, "The old woman left bark outside.",
     "Wulbuman-angka dumbul ngalkal-ba bawan.",
     "wulbuman-angka dumbul ngalkal-ba bawa-ny | old.woman-ERG bark outside-LOC leave-PAST",
     "r129_leave_bark_outside", 4, js(["wulbuman", "dumbul", "ngalkal", "bawal"]), js([]),
     "Dumbul bark object plus retained ngalkal-ba outside LOC.", "0.84"),
    (18, "I brought the stone from the house.",
     "Ngayu kulji bayan-muny wundin.",
     "ngayu kulji bayan-muny wundi-ny | I stone house-ABL bring-PAST",
     "r129_transfer_pronoun_stone_house_source", 4, js(["ngayu", "kulji", "bayan", "wundil"]), js([]),
     "Pronoun A bare; kulji stone object plus retained bayan-muny source.", "0.84"),
    (19, "We took sand to camp.",
     "Ngana jukar bayan-ka wundin.",
     "ngana jukar bayan-ka wundi-ny | we sand camp-DAT take-PAST",
     "r129_transfer_pronoun_sand_camp_goal", 4, js(["ngana", "jukar", "bayan", "wundil"]), js([]),
     "Pronoun A bare; jukar sand object plus retained bayan-ka goal.", "0.84"),
    (20, "Mother put the flower in the bag.",
     "Ngamu-ngku nganka balji-nga yijarrin.",
     "ngamu-ngku nganka balji-nga yijarri-ny | mother-ERG flower bag-LOC put-PAST",
     "r129_place_flower_bag", 4, js(["ngamu", "nganka", "balji", "yijarril"]), js([]),
     "Nganka flower object in retained placement frame.", "0.84"),

    (21, "The man saw smoke.",
     "Dingkar-angka kubu nyajin.",
     "dingkar-angka kubu nyaji-ny | man-ERG smoke see-PAST",
     "r129_perception_man_smoke", 4, js(["dingkar", "kubu", "nyajil"]), js([]),
     "Kubu smoke perceived object; common-noun perceiver ERG.", "0.84"),
    (22, "The child saw leaves on the tree.",
     "Karrkay-ngku birra juku-ngu nyajin.",
     "karrkay-ngku birra juku-ngu nyaji-ny | child-ERG leaf tree-LOC see-PAST",
     "r129_perception_child_leaf_tree", 4, js(["karrkay", "birra", "juku", "nyajil"]), js([]),
     "Birra leaf object plus sourced juku-ngu; common-noun perceiver ERG.", "0.84"),
    (23, "I saw leaves on the tree.",
     "Ngayu birra juku-ngu nyajin.",
     "ngayu birra juku-ngu nyaji-ny | I leaf tree-LOC see-PAST",
     "r129_perception_pronoun_leaf_tree", 4, js(["ngayu", "birra", "juku", "nyajil"]), js([]),
     "Pronoun A bare contrast for leaf perception.", "0.84"),
    (24, "The woman saw bark on the ground.",
     "Jalbu-ngku dumbul bubu-ngu nyajin.",
     "jalbu-ngku dumbul bubu-ngu nyaji-ny | woman-ERG bark ground-LOC see-PAST",
     "r129_perception_woman_bark_ground", 4, js(["jalbu", "dumbul", "bubu", "nyajil"]), js([]),
     "Dumbul bark perceived object; common-noun perceiver ERG.", "0.84"),
    (25, "We saw bark on the ground.",
     "Ngana dumbul bubu-ngu nyajin.",
     "ngana dumbul bubu-ngu nyaji-ny | we bark ground-LOC see-PAST",
     "r129_perception_pronoun_bark_ground", 4, js(["ngana", "dumbul", "bubu", "nyajil"]), js([]),
     "Pronoun A bare contrast for bark perception.", "0.84"),
    (26, "The man saw smoke from camp.",
     "Dingkar-angka kubu bayan-muny nyajin.",
     "dingkar-angka kubu bayan-muny nyaji-ny | man-ERG smoke camp-ABL see-PAST",
     "r129_perception_man_smoke_camp_source", 4, js(["dingkar", "kubu", "bayan", "nyajil"]), js([]),
     "Kubu smoke plus retained bayan-muny source; common-noun perceiver ERG.", "0.84"),
    (27, "I saw smoke outside.",
     "Ngayu kubu ngalkal-ba nyajin.",
     "ngayu kubu ngalkal-ba nyaji-ny | I smoke outside-LOC see-PAST",
     "r129_perception_pronoun_smoke_outside", 4, js(["ngayu", "kubu", "ngalkal", "nyajil"]), js([]),
     "Pronoun A bare; kubu smoke plus retained ngalkal-ba.", "0.84"),
    (28, "Grandmother saw the stone on the road.",
     "Babi-ngka kulji baral-ba nyajin.",
     "babi-ngka kulji baral-ba nyaji-ny | grandmother-ERG stone road-LOC see-PAST",
     "r129_perception_grandmother_stone_road", 4, js(["babi", "kulji", "baral", "nyajil"]), js([]),
     "Kulji stone perceived object; common-noun perceiver ERG.", "0.84"),
    (29, "We saw the stone on the road.",
     "Ngana kulji baral-ba nyajin.",
     "ngana kulji baral-ba nyaji-ny | we stone road-LOC see-PAST",
     "r129_perception_pronoun_stone_road", 4, js(["ngana", "kulji", "baral", "nyajil"]), js([]),
     "Pronoun A bare contrast for stone perception.", "0.84"),
    (30, "The young man saw the mountain.",
     "Warru-ngku manjal nyajin.",
     "warru-ngku manjal nyaji-ny | young.man-ERG mountain see-PAST",
     "r129_perception_young_man_mountain", 4, js(["warru", "manjal", "nyajil"]), js([]),
     "Manjal mountain, not kulji; common-noun perceiver ERG.", "0.84"),

    (31, "The old man asked mother about water.",
     "Wulman ngamu-ndu bana-ka babajin.",
     "wulman ngamu-ndu bana-ka babaji-ny | old.man mother-LOC:pt water-DAT ask-PAST",
     "r129_ask_old_man_mother_water", 4, js(["wulman", "ngamu", "bana", "babaji"]), js(["wulman"]),
     "Sourced ask-about frame: ABS/S asker, person LOC:pt, topic DAT; wulman contact form logged.", "0.86"),
    (32, "The young man asked father about water.",
     "Warru nganjan-anda bana-ka babajin.",
     "warru nganjan-anda bana-ka babaji-ny | young.man father-LOC:pt water-DAT ask-PAST",
     "r129_ask_young_man_father_water", 4, js(["warru", "nganjan", "bana", "babaji"]), js([]),
     "Sourced ask-about frame with retained bana-ka DAT topic.", "0.86"),
    (33, "The woman asked grandmother about news.",
     "Jalbu babi-nda kuku-ku babajin.",
     "jalbu babi-nda kuku-ku babaji-ny | woman grandmother-LOC:pt news-DAT ask-PAST",
     "r129_ask_woman_grandmother_news", 4, js(["jalbu", "babi", "kuku", "babaji"]), js([]),
     "Sourced ask-about frame with retained kuku-ku DAT topic.", "0.86"),
    (34, "The friend asked grandfather about meat.",
     "Jawun kami-nda minya-ka babajin.",
     "jawun kami-nda minya-ka babaji-ny | friend grandfather-LOC:pt meat-DAT ask-PAST",
     "r129_ask_friend_grandfather_meat", 4, js(["jawun", "kami", "minya", "babaji"]), js([]),
     "Sourced ask-about frame with retained minya-ka DAT topic.", "0.86"),
    (35, "The child asked mother about the mango.",
     "Karrkay ngamu-ndu mayngku-ku babajin.",
     "karrkay ngamu-ndu mayngku-ku babaji-ny | child mother-LOC:pt mango-DAT ask-PAST",
     "r129_ask_child_mother_mango", 4, js(["karrkay", "ngamu", "mayngku", "babaji"]), js(["mayngku"]),
     "Sourced ask-about frame; mayngku loan/contact form logged.", "0.86"),

    (36, "The man laughed at the girl.",
     "Dingkar maral-anda yajarrin.",
     "dingkar maral-anda yajarri-ny | man girl-DAT:pt laugh-PAST",
     "r129_laugh_man_girl", 3, js(["dingkar", "maral", "yajarril"]), js([]),
     "Retained yajarri-l laugh-target frame with sourced DAT:pt target.", "0.84"),
    (37, "The woman laughed at grandfather.",
     "Jalbu kami-nda yajarrin.",
     "jalbu kami-nda yajarri-ny | woman grandfather-DAT:pt laugh-PAST",
     "r129_laugh_woman_grandfather", 3, js(["jalbu", "kami", "yajarril"]), js([]),
     "Retained yajarri-l laugh-target frame with sourced DAT:pt target.", "0.84"),
    (38, "The young man laughed at grandmother.",
     "Warru babi-nda yajarrin.",
     "warru babi-nda yajarri-ny | young.man grandmother-DAT:pt laugh-PAST",
     "r129_laugh_young_man_grandmother", 3, js(["warru", "babi", "yajarril"]), js([]),
     "Retained yajarri-l laugh-target frame with sourced DAT:pt target.", "0.84"),
    (39, "Grandmother laughed at mother.",
     "Babi ngamu-ndu yajarrin.",
     "babi ngamu-ndu yajarri-ny | grandmother mother-DAT:pt laugh-PAST",
     "r129_laugh_grandmother_mother", 3, js(["babi", "ngamu", "yajarril"]), js([]),
     "Retained yajarri-l laugh-target frame with sourced DAT:pt target.", "0.84"),
    (40, "The child laughed at father.",
     "Karrkay nganjan-anda yajarrin.",
     "karrkay nganjan-anda yajarri-ny | child father-DAT:pt laugh-PAST",
     "r129_laugh_child_father", 3, js(["karrkay", "nganjan", "yajarril"]), js([]),
     "Retained yajarri-l laugh-target frame with sourced DAT:pt target.", "0.84"),

    (41, "Grandmother went to the scrub with the baby.",
     "Babi ngawa-nji madja-nga dungan.",
     "babi ngawa-nji madja-nga dunga-ny | grandmother baby-COMIT:pt scrub-LOC go-PAST",
     "r129_comit_grandmother_baby_scrub", 4, js(["babi", "ngawa", "madja", "dungay"]), js([]),
     "Ngawa-nji closed comitative; madja-nga retained LOC.", "0.84"),
    (42, "The young man went to the mountain with a friend.",
     "Warru jawun-unji manjal-ba dungan.",
     "warru jawun-unji manjal-ba dunga-ny | young.man friend-COMIT:pt mountain-LOC go-PAST",
     "r129_comit_young_man_friend_mountain", 4, js(["warru", "jawun", "manjal", "dungay"]), js([]),
     "Jawun-unji closed comitative; manjal-ba retained LOC.", "0.84"),
    (43, "The woman went into the water with father.",
     "Jalbu nganjan-anji bana-nga dungan.",
     "jalbu nganjan-anji bana-nga dunga-ny | woman father-COMIT:pt water-LOC go-PAST",
     "r129_comit_woman_father_water", 4, js(["jalbu", "nganjan", "bana", "dungay"]), js([]),
     "Nganjan-anji closed comitative; bana-nga retained LOC.", "0.84"),
    (44, "The child went to the scrub with a stick.",
     "Karrkay juku-ji madja-nga dungan.",
     "karrkay juku-ji madja-nga dunga-ny | child stick-COMIT scrub-LOC go-PAST",
     "r129_comit_child_stick_scrub", 4, js(["karrkay", "juku", "madja", "dungay"]), js([]),
     "Juku-ji allowed only as a closed sourced comitative; madja-nga retained LOC.", "0.84"),
    (45, "Mother went to the tree with clothes.",
     "Ngamu kambi-ji juku-ngu dungan.",
     "ngamu kambi-ji juku-ngu dunga-ny | mother clothes-COMIT tree-LOC go-PAST",
     "r129_comit_mother_clothes_tree", 4, js(["ngamu", "kambi", "juku", "dungay"]), js([]),
     "Kambi-ji closed comitative; juku-ngu retained LOC.", "0.84"),
    (46, "The old man stayed in the scrub with the baby.",
     "Wulman madja-nga bundanday ngawa-nji.",
     "wulman madja-nga bunda-n-bunda-y ngawa-nji | old.man scrub-LOC stay-REDUP-NONPAST baby-COMIT:pt",
     "r129_comit_old_man_baby_scrub", 4, js(["wulman", "madja", "bunday", "ngawa"]), js(["wulman"]),
     "Madja-nga retained LOC; ngawa-nji closed comitative; wulman contact form logged.", "0.84"),

    (47, "The man saw bark outside.",
     "Dingkar-angka dumbul ngalkal-ba nyajin.",
     "dingkar-angka dumbul ngalkal-ba nyaji-ny | man-ERG bark outside-LOC see-PAST",
     "r129_perception_man_bark_outside", 4, js(["dingkar", "dumbul", "ngalkal", "nyajil"]), js([]),
     "Dumbul bark perceived object; common-noun perceiver ERG.", "0.84"),
    (48, "The old woman saw seeds in the bag.",
     "Wulbuman-angka diburr balji-nga nyajin.",
     "wulbuman-angka diburr balji-nga nyaji-ny | old.woman-ERG seed bag-LOC see-PAST",
     "r129_perception_old_woman_seed_bag", 4, js(["wulbuman", "diburr", "balji", "nyajil"]), js([]),
     "Diburr seed sense selected; common-noun perceiver ERG.", "0.84"),
    (49, "I saw the flower on the tree.",
     "Ngayu nganka juku-ngu nyajin.",
     "ngayu nganka juku-ngu nyaji-ny | I flower tree-LOC see-PAST",
     "r129_perception_pronoun_flower_tree", 4, js(["ngayu", "nganka", "juku", "nyajil"]), js([]),
     "Pronoun A bare; nganka flower plus sourced juku-ngu.", "0.84"),
    (50, "We saw fog outside.",
     "Ngana ngurrbal ngalkal-ba nyajin.",
     "ngana ngurrbal ngalkal-ba nyaji-ny | we fog outside-LOC see-PAST",
     "r129_perception_pronoun_fog_outside", 4, js(["ngana", "ngurrbal", "ngalkal", "nyajil"]), js([]),
     "Pronoun A bare; ngurrbal fog plus retained ngalkal-ba.", "0.84"),
]


def has_token(low: str, token: str) -> bool:
    return re.search(rf"(?<![a-z-]){re.escape(token)}(?![a-z-])", 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 in enumerate(ROWS, start=1):
        seq, english, kuku, analysis, frame, _tier, words_used, loans_used, evidence, confidence = row
        label = f"{LABEL}.{seq:02d}"
        low = kuku.lower()
        english_low = english.lower()
        analysis_low = analysis.lower()
        try:
            json.loads(words_used)
            loan_values = json.loads(loans_used)
        except json.JSONDecodeError as exc:
            issues.append(f"{label}: JSON field failed: {exc}")
            loan_values = []
        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("r129_"):
            issues.append(f"{label}: Rule 129 batch frame must start with r129_")
        if low in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface within batch")
        seen_ku.add(low)
        for marker in CONSTRUCTION_CLITIC_MARKERS:
            if marker in low:
                issues.append(f"{label}: lexical batch must not reuse temporal clitic marker {marker}")
        for denied in DENY:
            if has_token(low, denied):
                issues.append(f"{label}: denied or out-of-scope token {denied!r}")
        for loan in LOAN_REQUIRED:
            if has_token(low, loan) and loan not in loan_values:
                issues.append(f"{label}: {loan} must be listed in loans_used")
        sense_pairs = [
            ("leaf", "birra"),
            ("leaves", "birra"),
            ("bark", "dumbul"),
            ("smoke", "kubu"),
            ("stone", "kulji"),
            ("sand", "jukar"),
            ("mountain", "manjal"),
            ("scrub", "madja"),
            ("seed", "diburr"),
            ("seeds", "diburr"),
            ("flower", "nganka"),
            ("fog", "ngurrbal"),
        ]
        for english_head, kuku_head in sense_pairs:
            if english_head in english_low and kuku_head not in low:
                issues.append(f"{label}: English {english_head} row must use {kuku_head}")
        if has_token(low, "birra") and "leaf" not in analysis_low:
            issues.append(f"{label}: birra must be glossed leaf, not lungs")
        if has_token(low, "dumbul") and not any(word in analysis_low for word in ("bark", "covering")):
            issues.append(f"{label}: dumbul must be glossed bark/hard covering")
        if has_token(low, "diburr") and "seed" not in analysis_low:
            issues.append(f"{label}: diburr must use the seed sense in this batch")
        if has_token(low, "kulji") and not any(word in analysis_low for word in ("stone", "rock")):
            issues.append(f"{label}: kulji must be glossed stone/rock")
        if "mountain" in english_low and has_token(low, "kulji"):
            issues.append(f"{label}: English mountain must use manjal, not kulji")
        if "kulji-bu" in low:
            issues.append(f"{label}: no kulji-bu instrument in Rule 129")
        if "INST" in analysis:
            issues.append(f"{label}: no instrument analysis in Rule 129 lexical expansion")
        if "babajin" in low and re.match(r"^[A-Za-z-]+-(?:ngka|angka|ngku)\b", kuku):
            issues.append(f"{label}: babaji-y asker drifted into ergative A marking")
        if "nyaji" in analysis_low:
            first = low.split()[0].strip(".,!?")
            if first not in PRONOUN_A and not re.search(r"-(?:ngka|angka|ngku)\b", first):
                issues.append(f"{label}: common-noun perceiver with nyaji-l should be ERG-marked")
        if "yajarrin" in low and not any(suffix in low for suffix in LAUGH_TARGET_SUFFIXES):
            issues.append(f"{label}: laugh target should show sourced DAT:pt target suffix")
        if "COMIT" in analysis and not any(form in low for form in SOURCED_COMITATIVES):
            issues.append(f"{label}: comitative form is not in the closed sourced set")
        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 129 draft: material and landscape lexical expansion", "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 129 source pass rechecked dictionary.yaml, Patz grammar, existing corpus lexemes, and planned-surface "
            "duplicates before authoring. Ten concrete material/landscape heads were selected and weak warrur evidence "
            "was excluded from this batch."
        ),
        command="rg dictionary.yaml and grammar.md; sqlite lexeme readback; exact duplicate scan over 50 planned rows",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="dictionary-source-check",
        actor="claude-main",
        detail=(
            "Dictionary controls: birra leaf, dumbul bark/hard covering, kubu smoke, kulji pebbles/small stones, "
            "jukar sand, madja scrub/rainforest, manjal mountain, diburr seed/egg with seed selected, nganka flower, "
            "and ngurrbal fog."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="grammar-source-check",
        actor="claude-main",
        detail=(
            "Grammar/corpus controls retained kulji/kulji-nga and the not-mountain warning, manjal-ba/manjal-muny, "
            "jukar-iji as evidence but not productive in this batch, nganka examples, birra-jirr-ka, kubu smoke, "
            "diburr seed/egg examples, and old frames for rest, transfer, perception, ask-about, laugh, and comitative."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Controls: birra leaf not lungs; dumbul bark only; diburr seed only; kulji stone/rock not mountain; "
            "manjal mountain; no kulji-bu or other instrument rows; no new comitatives or possessives; wulman/mayngku "
            "loan-contact forms logged; common-noun nyaji-l perceivers ERG-marked; babaji-y askers not ERG-marked."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="existing-surface-scan",
        actor="claude-main",
        detail="Final planned-surface scan found 50 planned rows, 50 unique within-batch surfaces, and zero exact cross-corpus duplicates.",
        command="sqlite exact-surface query over planned Batch 171 rows",
        output_excerpt="planned 50 unique 50; no duplicate lines emitted",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="process",
        event_type="process-capture-expanded",
        actor="claude-main",
        detail=(
            "Batch 171 stores source inventory, excluded weak warrur decision, static guards, duplicate scan, row grouping, "
            "bank outcome, and later lint/review/close/export/docs evidence as process_events plus this process log."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    db.executemany(
        """
        INSERT INTO sentences(
            batch_id, seq, english, kuku, analysis, frame, tier, words_used,
            loans_used, evidence, status, confidence, rights_status, author,
            created_at, updated_at
        ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        [
            (
                batch_id, seq, english, kuku, analysis, frame, tier, words_used,
                loans_used, evidence, "under_review", confidence,
                "project_approved_synthetic_pending_elder_verification",
                "claude-main", NOW, NOW,
            )
            for seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, confidence in ROWS
        ],
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="bank-success",
        actor="claude-main",
        detail="Banked 50 hand-authored Rule 129 material/landscape lexical rows under_review after static guards and duplicate checks.",
        command="python3 batch_scripts/bank_171.py",
        artifact_path="batch_scripts/bank_171.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-10 rest/come/stand/motion; 11-20 placement/leave/transfer; 21-30 perception; "
            "31-35 ask-about DAT topics; 36-40 laugh targets; 41-46 closed comitatives; 47-50 added perception."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    db.commit()

    count = db.execute("SELECT COUNT(*) FROM sentences WHERE batch_id=?", (batch_id,)).fetchone()[0]
    avg_words = db.execute(
        "SELECT AVG(LENGTH(kuku)-LENGTH(REPLACE(kuku,' ',''))+1.0) FROM sentences WHERE batch_id=?",
        (batch_id,),
    ).fetchone()[0]
    process_events = db.execute("SELECT COUNT(*) FROM process_events WHERE batch_label=?", (LABEL,)).fetchone()[0]
    print(f"banked {LABEL}: {count} rows under_review, avg_words={avg_words:.2f}, process_events={process_events}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
