#!/usr/bin/env python3
"""Bank batch-181: Rule 139 camp/fire-material lexical expansion."""

from __future__ import annotations

import datetime as dt
import json
import re
import sqlite3

from process_ledger import ensure_schema, record_event

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

TARGET_HEADS = {
    "jimal": "fire.stick",
    "ngunjil": "charcoal",
    "dumbul": "bark",
    "buyku": "paperbark",
    "jikan": "grass",
    "baya": "fire",
    "kubu": "smoke",
    "nganjirr": "grass.tree",
}
STATE_HEADS = {
    "ngulkurr": "good",
    "marral": "dry",
    "yalbay": "big",
}
SUPPORT_HEADS = {
    "balji": "bag",
    "bubu": "ground",
    "wabarr": "shade",
    "bayan": "camp",
}
APPROVED_LOCAL_FORMS = {"balji-nga", "bubu-ngu", "wabarr-ba", "bayan-ba", "bubu-muny"}
NONPORTABLE_TARGETS = {"baya", "kubu", "nganjirr"}
PORTABLE_VERBS = ("yijarrin", "bawan", "manin")
DENY = {
    "bajar",
    "ngalku",
    "dakil",
    "waba",
    "jandal",
    "jibarr",
    "malbin",
    "jiba",
    "bangkarr",
    "minya",
    "jungkalu",
    "juku",
    "kulji",
    "kambi",
    "badur",
    "karra",
    "makarr",
    "ngunyin",
    "wangal",
    "warrbi",
    "kunjuri",
    "kalka",
    "yah",
    "ngawuya",
    "walnga",
    "maja-nga",
    "yunga",
    "bada-bada",
    "wangkar-wangkar",
    "biyul",
    "warrur",
}
FORBIDDEN_MARKERS = ("-ngarr", "-ngurr", "-kuda", "-l-urr", "wawu-")
PRONOUN_A = {"ngayu", "ngana", "nganjin", "yundu", "yurra", "jana", "nyulu"}
TRANSITIVE_PAST = ("nyajin", "yijarrin", "bawan", "manin")


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


ROWS = [
    (1, "The fire stick is good.",
     "Jimal ngulkurr.",
     "jimal ngulkurr | fire.stick good",
     "r139_state_fire_stick_good", 2, js(["jimal", "ngulkurr"]), js([]),
     "Rule 139 lexical expansion: dictionary jimal fire stick with verified ngulkurr good.", "0.86"),
    (2, "The fire stick is dry.",
     "Jimal marral.",
     "jimal marral | fire.stick dry",
     "r139_state_fire_stick_dry", 2, js(["jimal", "marral"]), js([]),
     "Dictionary jimal fire stick; marral dry is previously verified for practical material states.", "0.86"),
    (3, "The bark is dry.",
     "Dumbul marral.",
     "dumbul marral | bark dry",
     "r139_state_bark_dry", 2, js(["dumbul", "marral"]), js([]),
     "Dictionary dumbul bark/hard covering; selected here as bark with marral dry.", "0.86"),
    (4, "The paperbark is dry.",
     "Buyku marral.",
     "buyku marral | paperbark dry",
     "r139_state_paperbark_dry", 2, js(["buyku", "marral"]), js([]),
     "Dictionary buyku paperbark; grammar Text 51 has paperbark in earth-oven context.", "0.86"),
    (5, "The grass is good.",
     "Jikan ngulkurr.",
     "jikan ngulkurr | grass good",
     "r139_state_grass_good", 2, js(["jikan", "ngulkurr"]), js([]),
     "Dictionary jikan grass; ngulkurr good state avoids duplicate Jikan marral.", "0.84"),
    (6, "The firewood is dry.",
     "Baya marral.",
     "baya marral | firewood dry",
     "r139_state_firewood_dry", 2, js(["baya", "marral"]), js([]),
     "Dictionary and grammar allow baya fire/firewood; English selects firewood for dry material state.", "0.84"),
    (7, "The smoke is big.",
     "Kubu yalbay.",
     "kubu yalbay | smoke big",
     "r139_state_smoke_big", 2, js(["kubu", "yalbay"]), js([]),
     "Dictionary kubu smoke; yalbay big is a previously verified size predicate.", "0.84"),
    (8, "The grass tree is big.",
     "Nganjirr yalbay.",
     "nganjirr yalbay | grass.tree big",
     "r139_state_grass_tree_big", 2, js(["nganjirr", "yalbay"]), js([]),
     "Dictionary nganjirr grass tree; grammar ex.65 confirms simple root nganjirr.", "0.86"),
    (9, "The fire stick lay on the ground.",
     "Jimal bubu-ngu wunan.",
     "jimal bubu-ngu wuna-ny | fire.stick ground-LOC lie-PAST",
     "r139_rest_fire_stick_ground", 3, js(["jimal", "bubu", "wunay"]), js([]),
     "Jimal fire stick in rest-location frame with source-backed bubu-ngu ground LOC.", "0.84"),
    (10, "The fire stick lay in the shade.",
     "Jimal wabarr-ba wunan.",
     "jimal wabarr-ba wuna-ny | fire.stick shade-LOC lie-PAST",
     "r139_rest_fire_stick_shade", 3, js(["jimal", "wabarr", "wunay"]), js([]),
     "Jimal fire stick with wabarr-ba shade LOC and wuna-y past.", "0.84"),
    (11, "The charcoal lay on the ground.",
     "Ngunjil bubu-ngu wunan.",
     "ngunjil bubu-ngu wuna-ny | charcoal ground-LOC lie-PAST",
     "r139_rest_charcoal_ground", 3, js(["ngunjil", "bubu", "wunay"]), js([]),
     "Dictionary ngunjil charcoal/hot coals; selected as charcoal in ground-location frame.", "0.84"),
    (12, "The charcoal lay at camp.",
     "Ngunjil bayan-ba wunan.",
     "ngunjil bayan-ba wuna-ny | charcoal camp-LOC lie-PAST",
     "r139_rest_charcoal_camp", 3, js(["ngunjil", "bayan", "wunay"]), js([]),
     "Ngunjil selected as charcoal; bayan-ba camp/house LOC is support only.", "0.84"),
    (13, "The bark lay at camp.",
     "Dumbul bayan-ba wunan.",
     "dumbul bayan-ba wuna-ny | bark camp-LOC lie-PAST",
     "r139_rest_bark_camp", 3, js(["dumbul", "bayan", "wunay"]), js([]),
     "Dumbul bark with bayan-ba support LOC; no shell/crust sense intended.", "0.84"),
    (14, "The paperbark lay at camp.",
     "Buyku bayan-ba wunan.",
     "buyku bayan-ba wuna-ny | paperbark camp-LOC lie-PAST",
     "r139_rest_paperbark_camp", 3, js(["buyku", "bayan", "wunay"]), js([]),
     "Buyku paperbark as rest subject with bayan-ba camp LOC.", "0.84"),
    (15, "The grass lay at camp.",
     "Jikan bayan-ba wunan.",
     "jikan bayan-ba wuna-ny | grass camp-LOC lie-PAST",
     "r139_rest_grass_camp", 3, js(["jikan", "bayan", "wunay"]), js([]),
     "Jikan grass as bare subject; bayan-ba is support location, not target morphology.", "0.82"),
    (16, "The grass tree is good.",
     "Nganjirr ngulkurr.",
     "nganjirr ngulkurr | grass.tree good",
     "r139_state_grass_tree_good", 2, js(["nganjirr", "ngulkurr"]), js([]),
     "Nganjirr kept as grass tree; state frame avoids treating it as a portable object.", "0.86"),
    (17, "Mother saw the fire stick on the ground.",
     "Ngamu-ngku jimal bubu-ngu nyajin.",
     "ngamu-ngku jimal bubu-ngu nyaji-ny | mother-ERG fire.stick ground-LOC see-PAST",
     "r139_perception_mother_fire_stick_ground", 4, js(["ngamu", "jimal", "bubu", "nyajil"]), js([]),
     "Common-noun A ERG; jimal fire stick object plus bubu-ngu support LOC.", "0.84"),
    (18, "Father saw the charcoal on the ground.",
     "Nganjan-angka ngunjil bubu-ngu nyajin.",
     "nganjan-angka ngunjil bubu-ngu nyaji-ny | father-ERG charcoal ground-LOC see-PAST",
     "r139_perception_father_charcoal_ground", 4, js(["nganjan", "ngunjil", "bubu", "nyajil"]), js([]),
     "Common-noun A ERG; ngunjil selected as charcoal/hot-coals noun.", "0.84"),
    (19, "Grandmother saw the bark at camp.",
     "Babi-ngka dumbul bayan-ba nyajin.",
     "babi-ngka dumbul bayan-ba nyaji-ny | grandmother-ERG bark camp-LOC see-PAST",
     "r139_perception_grandmother_bark_camp", 4, js(["babi", "dumbul", "bayan", "nyajil"]), js([]),
     "Common-noun A ERG; dumbul selected as bark.", "0.84"),
    (20, "The child saw the paperbark on the ground.",
     "Karrkay-ngku buyku bubu-ngu nyajin.",
     "karrkay-ngku buyku bubu-ngu nyaji-ny | child-ERG paperbark ground-LOC see-PAST",
     "r139_perception_child_paperbark_ground", 4, js(["karrkay", "buyku", "bubu", "nyajil"]), js([]),
     "Common-noun A ERG; buyku paperbark with ground LOC.", "0.84"),
    (21, "Grandfather saw the grass in the shade.",
     "Kami-ngka jikan wabarr-ba nyajin.",
     "kami-ngka jikan wabarr-ba nyaji-ny | grandfather-ERG grass shade-LOC see-PAST",
     "r139_perception_grandfather_grass_shade", 4, js(["kami", "jikan", "wabarr", "nyajil"]), js([]),
     "Common-noun A ERG; jikan remains bare grass object, wabarr-ba is support LOC.", "0.84"),
    (22, "The woman saw the fire.",
     "Jalbu-ngku baya nyajin.",
     "jalbu-ngku baya nyaji-ny | woman-ERG fire see-PAST",
     "r139_perception_woman_fire", 4, js(["jalbu", "baya", "nyajil"]), js([]),
     "Grammar text has baya as seen fire; common-noun A is ERG-marked.", "0.84"),
    (23, "Grandfather saw the smoke.",
     "Kami-ngka kubu nyajin.",
     "kami-ngka kubu nyaji-ny | grandfather-ERG smoke see-PAST",
     "r139_perception_grandfather_smoke", 4, js(["kami", "kubu", "nyajil"]), js([]),
     "Dictionary kubu smoke and grammar has baya kubu nyaji-nka 'see smoke'.", "0.84"),
    (24, "I saw the grass tree.",
     "Ngayu nganjirr nyajin.",
     "ngayu nganjirr nyaji-ny | I grass.tree see-PAST",
     "r139_perception_i_grass_tree", 4, js(["ngayu", "nganjirr", "nyajil"]), js([]),
     "Pronoun A bare; nganjirr selected as grass tree.", "0.84"),
    (25, "They saw the fire at camp.",
     "Jana baya bayan-ba nyajin.",
     "jana baya bayan-ba nyaji-ny | they fire camp-LOC see-PAST",
     "r139_perception_they_fire_camp", 4, js(["jana", "baya", "bayan", "nyajil"]), js([]),
     "Pronoun A bare; baya selected as fire, bayan-ba support LOC.", "0.84"),
    (26, "We saw the smoke at camp.",
     "Ngana kubu bayan-ba nyajin.",
     "ngana kubu bayan-ba nyaji-ny | we smoke camp-LOC see-PAST",
     "r139_perception_we_smoke_camp", 4, js(["ngana", "kubu", "bayan", "nyajil"]), js([]),
     "Pronoun A bare; kubu smoke with bayan-ba camp LOC.", "0.84"),
    (27, "Mother saw the bark in the bag.",
     "Ngamu-ngku dumbul balji-nga nyajin.",
     "ngamu-ngku dumbul balji-nga nyaji-ny | mother-ERG bark bag-LOC see-PAST",
     "r139_perception_mother_bark_bag", 4, js(["ngamu", "dumbul", "balji", "nyajil"]), js([]),
     "Common-noun A ERG; dumbul bark with Patz-sourced balji-nga bag LOC.", "0.84"),
    (28, "Father saw the paperbark in the shade.",
     "Nganjan-angka buyku wabarr-ba nyajin.",
     "nganjan-angka buyku wabarr-ba nyaji-ny | father-ERG paperbark shade-LOC see-PAST",
     "r139_perception_father_paperbark_shade", 4, js(["nganjan", "buyku", "wabarr", "nyajil"]), js([]),
     "Common-noun A ERG; buyku paperbark plus wabarr-ba support LOC.", "0.84"),
    (29, "Grandmother put the fire stick on the ground.",
     "Babi-ngka jimal bubu-ngu yijarrin.",
     "babi-ngka jimal bubu-ngu yijarri-ny | grandmother-ERG fire.stick ground-LOC put-PAST",
     "r139_place_grandmother_fire_stick_ground", 4, js(["babi", "jimal", "bubu", "yijarril"]), js([]),
     "Yijarri-l put/place with common-noun A ERG and jimal bare object.", "0.84"),
    (30, "The child put the charcoal on the ground.",
     "Karrkay-ngku ngunjil bubu-ngu yijarrin.",
     "karrkay-ngku ngunjil bubu-ngu yijarri-ny | child-ERG charcoal ground-LOC put-PAST",
     "r139_place_child_charcoal_ground", 4, js(["karrkay", "ngunjil", "bubu", "yijarril"]), js([]),
     "Yijarri-l put/place with ngunjil selected as charcoal, not ash residue.", "0.82"),
    (31, "Grandfather put the bark at camp.",
     "Kami-ngka dumbul bayan-ba yijarrin.",
     "kami-ngka dumbul bayan-ba yijarri-ny | grandfather-ERG bark camp-LOC put-PAST",
     "r139_place_grandfather_bark_camp", 4, js(["kami", "dumbul", "bayan", "yijarril"]), js([]),
     "Dumbul bark bare object; bayan-ba support LOC.", "0.84"),
    (32, "The woman put the paperbark in the shade.",
     "Jalbu-ngku buyku wabarr-ba yijarrin.",
     "jalbu-ngku buyku wabarr-ba yijarri-ny | woman-ERG paperbark shade-LOC put-PAST",
     "r139_place_woman_paperbark_shade", 4, js(["jalbu", "buyku", "wabarr", "yijarril"]), js([]),
     "Buyku paperbark bare object in placement frame.", "0.84"),
    (33, "The man put the grass at camp.",
     "Dingkar-angka jikan bayan-ba yijarrin.",
     "dingkar-angka jikan bayan-ba yijarri-ny | man-ERG grass camp-LOC put-PAST",
     "r139_place_man_grass_camp", 4, js(["dingkar", "jikan", "bayan", "yijarril"]), js([]),
     "Jikan grass bare object; no jikan locative target form.", "0.82"),
    (34, "Mother put the fire stick in the bag.",
     "Ngamu-ngku jimal balji-nga yijarrin.",
     "ngamu-ngku jimal balji-nga yijarri-ny | mother-ERG fire.stick bag-LOC put-PAST",
     "r139_place_mother_fire_stick_bag", 4, js(["ngamu", "jimal", "balji", "yijarril"]), js([]),
     "Jimal portable object in bag-location frame; balji remains support-only.", "0.84"),
    (35, "Father put the bark in the shade.",
     "Nganjan-angka dumbul wabarr-ba yijarrin.",
     "nganjan-angka dumbul wabarr-ba yijarri-ny | father-ERG bark shade-LOC put-PAST",
     "r139_place_father_bark_shade", 4, js(["nganjan", "dumbul", "wabarr", "yijarril"]), js([]),
     "Dumbul selected as bark; wabarr-ba shade LOC support.", "0.84"),
    (36, "They put the paperbark on the ground.",
     "Jana buyku bubu-ngu yijarrin.",
     "jana buyku bubu-ngu yijarri-ny | they paperbark ground-LOC put-PAST",
     "r139_place_they_paperbark_ground", 4, js(["jana", "buyku", "bubu", "yijarril"]), js([]),
     "Pronoun A bare; buyku paperbark object plus bubu-ngu.", "0.84"),
    (37, "Mother left the fire stick in the shade.",
     "Ngamu-ngku jimal wabarr-ba bawan.",
     "ngamu-ngku jimal wabarr-ba bawa-ny | mother-ERG fire.stick shade-LOC leave-PAST",
     "r139_leave_mother_fire_stick_shade", 4, js(["ngamu", "jimal", "wabarr", "bawal"]), js([]),
     "Bawa-l leave with jimal fire stick object and shade LOC.", "0.84"),
    (38, "Father left the charcoal on the ground.",
     "Nganjan-angka ngunjil bubu-ngu bawan.",
     "nganjan-angka ngunjil bubu-ngu bawa-ny | father-ERG charcoal ground-LOC leave-PAST",
     "r139_leave_father_charcoal_ground", 4, js(["nganjan", "ngunjil", "bubu", "bawal"]), js([]),
     "Ngunjil selected as charcoal; bawa-l leave with ground LOC.", "0.82"),
    (39, "Grandmother left the bark at camp.",
     "Babi-ngka dumbul bayan-ba bawan.",
     "babi-ngka dumbul bayan-ba bawa-ny | grandmother-ERG bark camp-LOC leave-PAST",
     "r139_leave_grandmother_bark_camp", 4, js(["babi", "dumbul", "bayan", "bawal"]), js([]),
     "Dumbul bark bare object in leave-location frame.", "0.84"),
    (40, "The child left the paperbark in the shade.",
     "Karrkay-ngku buyku wabarr-ba bawan.",
     "karrkay-ngku buyku wabarr-ba bawa-ny | child-ERG paperbark shade-LOC leave-PAST",
     "r139_leave_child_paperbark_shade", 4, js(["karrkay", "buyku", "wabarr", "bawal"]), js([]),
     "Buyku paperbark object with wabarr-ba support LOC.", "0.84"),
    (41, "Grandfather left the grass at camp.",
     "Kami-ngka jikan bayan-ba bawan.",
     "kami-ngka jikan bayan-ba bawa-ny | grandfather-ERG grass camp-LOC leave-PAST",
     "r139_leave_grandfather_grass_camp", 4, js(["kami", "jikan", "bayan", "bawal"]), js([]),
     "Jikan grass bare object; no target local case.", "0.82"),
    (42, "The woman left the fire stick in the bag.",
     "Jalbu-ngku jimal balji-nga bawan.",
     "jalbu-ngku jimal balji-nga bawa-ny | woman-ERG fire.stick bag-LOC leave-PAST",
     "r139_leave_woman_fire_stick_bag", 4, js(["jalbu", "jimal", "balji", "bawal"]), js([]),
     "Jimal fire stick in balji-nga bag support frame.", "0.84"),
    (43, "The man left the charcoal in the shade.",
     "Dingkar-angka ngunjil wabarr-ba bawan.",
     "dingkar-angka ngunjil wabarr-ba bawa-ny | man-ERG charcoal shade-LOC leave-PAST",
     "r139_leave_man_charcoal_shade", 4, js(["dingkar", "ngunjil", "wabarr", "bawal"]), js([]),
     "Ngunjil charcoal with wabarr-ba; no ash-residue head used.", "0.82"),
    (44, "I left the bark at camp.",
     "Ngayu dumbul bayan-ba bawan.",
     "ngayu dumbul bayan-ba bawa-ny | I bark camp-LOC leave-PAST",
     "r139_leave_i_bark_camp", 4, js(["ngayu", "dumbul", "bayan", "bawal"]), js([]),
     "Pronoun A bare; dumbul bark object plus bayan-ba support LOC.", "0.84"),
    (45, "Mother took the fire stick from the ground.",
     "Ngamu-ngku jimal bubu-muny manin.",
     "ngamu-ngku jimal bubu-muny mani-ny | mother-ERG fire.stick ground-ABL get-PAST",
     "r139_get_mother_fire_stick_ground", 4, js(["ngamu", "jimal", "bubu", "manil"]), js([]),
     "Mani-l get/take with jimal fire stick and bubu-muny source.", "0.84"),
    (46, "Father took the bark from the ground.",
     "Nganjan-angka dumbul bubu-muny manin.",
     "nganjan-angka dumbul bubu-muny mani-ny | father-ERG bark ground-ABL get-PAST",
     "r139_get_father_bark_ground", 4, js(["nganjan", "dumbul", "bubu", "manil"]), js([]),
     "Dumbul bark object with bubu-muny source.", "0.84"),
    (47, "Grandmother took the paperbark from the ground.",
     "Babi-ngka buyku bubu-muny manin.",
     "babi-ngka buyku bubu-muny mani-ny | grandmother-ERG paperbark ground-ABL get-PAST",
     "r139_get_grandmother_paperbark_ground", 4, js(["babi", "buyku", "bubu", "manil"]), js([]),
     "Buyku paperbark object with bubu-muny source.", "0.84"),
    (48, "The child took the grass from the ground.",
     "Karrkay-ngku jikan bubu-muny manin.",
     "karrkay-ngku jikan bubu-muny mani-ny | child-ERG grass ground-ABL get-PAST",
     "r139_get_child_grass_ground", 4, js(["karrkay", "jikan", "bubu", "manil"]), js([]),
     "Jikan grass object with bubu-muny source; no jikan target case.", "0.82"),
    (49, "I took the charcoal from the ground.",
     "Ngayu ngunjil bubu-muny manin.",
     "ngayu ngunjil bubu-muny mani-ny | I charcoal ground-ABL get-PAST",
     "r139_get_i_charcoal_ground", 4, js(["ngayu", "ngunjil", "bubu", "manil"]), js([]),
     "Pronoun A bare; ngunjil selected as charcoal, not bajar ash tree.", "0.82"),
    (50, "They took the fire stick from the ground.",
     "Jana jimal bubu-muny manin.",
     "jana jimal bubu-muny mani-ny | they fire.stick ground-ABL get-PAST",
     "r139_get_they_fire_stick_ground", 4, js(["jana", "jimal", "bubu", "manil"]), js([]),
     "Pronoun A bare; jimal fire stick object plus bubu-muny source.", "0.84"),
]


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


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


def has_ergative(word: str) -> bool:
    return bool(re.search(r"-(?:ngka|angka|ngku)\b", word))


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:
            words = json.loads(words_used)
            loans = json.loads(loans_used)
        except json.JSONDecodeError as exc:
            issues.append(f"{label}: JSON field failed: {exc}")
            words = []
            loans = []
        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("r139_"):
            issues.append(f"{label}: Rule 139 frame must start with r139_")
        if low in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface within batch")
        seen_ku.add(low)
        if loans:
            issues.append(f"{label}: Rule 139 batch should not introduce loans")
        for denied in DENY:
            if has_base(low, denied):
                issues.append(f"{label}: denied or out-of-scope token {denied!r}")
        for marker in FORBIDDEN_MARKERS:
            if marker in low:
                issues.append(f"{label}: forbidden construction marker {marker}")
        if "INST" in analysis or "-bu" in analysis_low or re.search(r"\b[a-z]+-bu\b", low):
            issues.append(f"{label}: no instrument rows in Rule 139")
        if "COMIT" in analysis:
            issues.append(f"{label}: no comitative rows in Rule 139")
        if re.search(r"-(?:amu|mu|ndamun|andamun)\b", low):
            issues.append(f"{label}: no alienable possessive suffixes in Rule 139")
        for form in re.findall(r"\b[a-z]+-(?:nga|ngu|ba|muny)\b", low):
            if form not in APPROVED_LOCAL_FORMS:
                issues.append(f"{label}: unapproved local-case support form {form!r}")
        if not any(has_base(low, head) for head in TARGET_HEADS):
            issues.append(f"{label}: Rule 139 row must exercise a camp/fire-material target")
        for head, gloss in TARGET_HEADS.items():
            if has_base(low, head):
                if head not in words:
                    issues.append(f"{label}: target head {head} missing from words_used")
                if gloss not in analysis_low:
                    issues.append(f"{label}: {head} must be glossed with selected sense {gloss!r}")
                if head in NONPORTABLE_TARGETS and any(verb in low for verb in PORTABLE_VERBS):
                    issues.append(f"{label}: {head} should not be treated as a portable handled object")
            if f"{head}-" in low:
                issues.append(f"{label}: no target-head case form or compound for {head}")
        for head, gloss in STATE_HEADS.items():
            if has_base(low, head):
                if head not in words:
                    issues.append(f"{label}: state head {head} missing from words_used")
                if gloss not in analysis_low:
                    issues.append(f"{label}: state head {head} should be glossed as {gloss!r}")
        for head in SUPPORT_HEADS:
            if has_base(low, head) and head not in words:
                issues.append(f"{label}: support head {head} missing from words_used")
        if has_base(low, "baya") and "firewood" in english_low and "firewood" not in analysis_low:
            issues.append(f"{label}: firewood English requires firewood analysis for baya")
        if has_base(low, "dumbul") and any(bad in english_low or bad in analysis_low for bad in ("shell", "crust")):
            issues.append(f"{label}: dumbul must stay bark in Rule 139")
        if has_base(low, "bajar"):
            issues.append(f"{label}: bajar is a Moreton Bay ash/white gum tree entry, not residue ash")
        first = first_word(low)
        if any(verb in low for verb in TRANSITIVE_PAST):
            if first not in PRONOUN_A and not has_ergative(first):
                issues.append(f"{label}: common-noun A in transitive past frame should be ERG-marked")
        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 139 draft: camp/fire-material lexical expansion after Rule 138", "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 139 source pass checked dictionary and Patz grammar for jimal fire stick, ngunjil charcoal/hot "
            "coals, dumbul bark/hard covering, buyku paperbark, jikan grass, baya fire/firewood, kubu smoke, "
            "and nganjirr grass tree. Bajar was rejected as residue ash because the dictionary entry is Moreton "
            "Bay ash/white gum tree, not ash residue."
        ),
        command="rg/sed dictionary.yaml and grammar.md; sqlite lexeme and exact-surface readback",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="lexical-expansion-scope",
        actor="claude-main",
        detail=(
            "Rule 139 admits eight camp/fire-material targets: jimal, ngunjil, dumbul, buyku, jikan, baya, kubu, "
            "and nganjirr. Fire/smoke/grass-tree rows are restricted to state/perception, except baya as "
            "firewood in one dry state row; portable-object verbs are limited to fire stick, charcoal, bark, "
            "paperbark, and grass."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Controls: no bajar residue-ash rows; no ngalku grassfire; no Rule 137/138 handled-object carry-over "
            "heads; no target-head case forms; no instruments, comitatives, possessives, temporal clitics, or "
            "unsourced analogies; approved support local forms only; common-noun A ERG and pronoun A bare."
        ),
        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. Earlier candidates Jikan marral and Dingkar-angka kubu nyajin were replaced "
            "after duplicate detection."
        ),
        command="sqlite exact-surface query over planned Batch 181 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 181 stores source readback, lexical-scope decisions, static guards, duplicate scan, 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-written Rule 139 camp/fire-material lexical-expansion rows as under_review after static and exact duplicate checks.",
        command="python3 batch_scripts/bank_181.py",
        output_excerpt="banked batch-181: 50 rows under_review; duplicate scan clean",
        artifact_path="batch_scripts/bank_181.py",
        created_at=NOW,
    )
    db.commit()
    count = db.execute("SELECT COUNT(*) FROM sentences WHERE batch_id=?", (batch_id,)).fetchone()[0]
    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={count}, under_review={under_review}, verified_before_close={verified}")
    return 0


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