#!/usr/bin/env python3
"""Bank batch-182: Rule 140 construction consolidation after Rule 139."""

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-182"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-182-construction-consolidation-after-rule139-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",
    "ngalkal": "outside",
}
APPROVED_LOCAL_FORMS = {"balji-nga", "bubu-ngu", "wabarr-ba", "bayan-ba", "bubu-muny", "ngalkal-ba"}
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 charcoal is good.",
     "Ngunjil ngulkurr.",
     "ngunjil ngulkurr | charcoal good",
     "r140_state_charcoal_good", 2, js(["ngunjil", "ngulkurr"]), js([]),
     "Rule 140 consolidation of Rule 139 ngunjil charcoal/hot coals with ngulkurr good.", "0.84"),
    (2, "The bark is good.",
     "Dumbul ngulkurr.",
     "dumbul ngulkurr | bark good",
     "r140_state_bark_good", 2, js(["dumbul", "ngulkurr"]), js([]),
     "Dumbul remains selected as bark, with the verified state predicate ngulkurr good.", "0.86"),
    (3, "The paperbark is good.",
     "Buyku ngulkurr.",
     "buyku ngulkurr | paperbark good",
     "r140_state_paperbark_good", 2, js(["buyku", "ngulkurr"]), js([]),
     "Buyku paperbark in a simple state frame.", "0.86"),
    (4, "The grass is big.",
     "Jikan yalbay.",
     "jikan yalbay | grass big",
     "r140_state_grass_big", 2, js(["jikan", "yalbay"]), js([]),
     "Jikan grass with yalbay big; no jikan target case form.", "0.82"),
    (5, "The fire stick is big.",
     "Jimal yalbay.",
     "jimal yalbay | fire.stick big",
     "r140_state_fire_stick_big", 2, js(["jimal", "yalbay"]), js([]),
     "Jimal fire stick in a size-state row.", "0.84"),
    (6, "The fire is big.",
     "Baya yalbay.",
     "baya yalbay | fire big",
     "r140_state_fire_big", 2, js(["baya", "yalbay"]), js([]),
     "Baya selected as fire; state frame avoids portable-object treatment.", "0.84"),
    (7, "The grass tree is dry.",
     "Nganjirr marral.",
     "nganjirr marral | grass.tree dry",
     "r140_state_grass_tree_dry", 2, js(["nganjirr", "marral"]), js([]),
     "Nganjirr selected as grass tree; state frame only.", "0.84"),
    (8, "The fire is good.",
     "Baya ngulkurr.",
     "baya ngulkurr | fire good",
     "r140_state_fire_good", 2, js(["baya", "ngulkurr"]), js([]),
     "Baya fire in a simple state frame; no firewood handling row.", "0.82"),

    (9, "The fire stick lay at camp.",
     "Jimal bayan-ba wunan.",
     "jimal bayan-ba wuna-ny | fire.stick camp-LOC lie-PAST",
     "r140_rest_fire_stick_camp", 3, js(["jimal", "bayan", "wunay"]), js([]),
     "Jimal fire stick with bayan-ba camp/house LOC.", "0.84"),
    (10, "The fire stick lay in the bag.",
     "Jimal balji-nga wunan.",
     "jimal balji-nga wuna-ny | fire.stick bag-LOC lie-PAST",
     "r140_rest_fire_stick_bag", 3, js(["jimal", "balji", "wunay"]), js([]),
     "Jimal fire stick with balji-nga support-only bag LOC.", "0.84"),
    (11, "The charcoal lay in the shade.",
     "Ngunjil wabarr-ba wunan.",
     "ngunjil wabarr-ba wuna-ny | charcoal shade-LOC lie-PAST",
     "r140_rest_charcoal_shade", 3, js(["ngunjil", "wabarr", "wunay"]), js([]),
     "Ngunjil charcoal with wabarr-ba shade LOC; no ash-residue wording.", "0.82"),
    (12, "The charcoal lay in the bag.",
     "Ngunjil balji-nga wunan.",
     "ngunjil balji-nga wuna-ny | charcoal bag-LOC lie-PAST",
     "r140_rest_charcoal_bag", 3, js(["ngunjil", "balji", "wunay"]), js([]),
     "Ngunjil selected as charcoal; balji-nga remains support location.", "0.82"),
    (13, "The bark lay outside.",
     "Dumbul ngalkal-ba wunan.",
     "dumbul ngalkal-ba wuna-ny | bark outside-LOC lie-PAST",
     "r140_rest_bark_outside", 3, js(["dumbul", "ngalkal", "wunay"]), js([]),
     "Dumbul bark with source-backed ngalkal-ba outside LOC.", "0.84"),
    (14, "The paperbark lay in the bag.",
     "Buyku balji-nga wunan.",
     "buyku balji-nga wuna-ny | paperbark bag-LOC lie-PAST",
     "r140_rest_paperbark_bag", 3, js(["buyku", "balji", "wunay"]), js([]),
     "Buyku paperbark with balji-nga support LOC.", "0.84"),
    (15, "The grass lay outside.",
     "Jikan ngalkal-ba wunan.",
     "jikan ngalkal-ba wuna-ny | grass outside-LOC lie-PAST",
     "r140_rest_grass_outside", 3, js(["jikan", "ngalkal", "wunay"]), js([]),
     "Jikan grass bare subject; ngalkal-ba support LOC.", "0.82"),
    (16, "The fire stick lay outside.",
     "Jimal ngalkal-ba wunan.",
     "jimal ngalkal-ba wuna-ny | fire.stick outside-LOC lie-PAST",
     "r140_rest_fire_stick_outside", 3, js(["jimal", "ngalkal", "wunay"]), js([]),
     "Jimal fire stick with ngalkal-ba outside LOC.", "0.84"),

    (17, "Mother saw the fire stick at camp.",
     "Ngamu-ngku jimal bayan-ba nyajin.",
     "ngamu-ngku jimal bayan-ba nyaji-ny | mother-ERG fire.stick camp-LOC see-PAST",
     "r140_perception_mother_fire_stick_camp", 4, js(["ngamu", "jimal", "bayan", "nyajil"]), js([]),
     "Common-noun A ERG; jimal object plus bayan-ba support LOC.", "0.84"),
    (18, "Father saw the charcoal in the shade.",
     "Nganjan-angka ngunjil wabarr-ba nyajin.",
     "nganjan-angka ngunjil wabarr-ba nyaji-ny | father-ERG charcoal shade-LOC see-PAST",
     "r140_perception_father_charcoal_shade", 4, js(["nganjan", "ngunjil", "wabarr", "nyajil"]), js([]),
     "Common-noun A ERG; ngunjil selected as charcoal.", "0.84"),
    (19, "Grandmother saw the bark outside.",
     "Babi-ngka dumbul ngalkal-ba nyajin.",
     "babi-ngka dumbul ngalkal-ba nyaji-ny | grandmother-ERG bark outside-LOC see-PAST",
     "r140_perception_grandmother_bark_outside", 4, js(["babi", "dumbul", "ngalkal", "nyajil"]), js([]),
     "Common-noun A ERG; dumbul bark with ngalkal-ba outside LOC.", "0.84"),
    (20, "The child saw the paperbark at camp.",
     "Karrkay-ngku buyku bayan-ba nyajin.",
     "karrkay-ngku buyku bayan-ba nyaji-ny | child-ERG paperbark camp-LOC see-PAST",
     "r140_perception_child_paperbark_camp", 4, js(["karrkay", "buyku", "bayan", "nyajil"]), js([]),
     "Common-noun A ERG; buyku paperbark with bayan-ba support.", "0.84"),
    (21, "Grandfather saw the grass outside.",
     "Kami-ngka jikan ngalkal-ba nyajin.",
     "kami-ngka jikan ngalkal-ba nyaji-ny | grandfather-ERG grass outside-LOC see-PAST",
     "r140_perception_grandfather_grass_outside", 4, js(["kami", "jikan", "ngalkal", "nyajil"]), js([]),
     "Common-noun A ERG; jikan grass object remains bare.", "0.84"),
    (22, "The woman saw the fire at camp.",
     "Jalbu-ngku baya bayan-ba nyajin.",
     "jalbu-ngku baya bayan-ba nyaji-ny | woman-ERG fire camp-LOC see-PAST",
     "r140_perception_woman_fire_camp", 4, js(["jalbu", "baya", "bayan", "nyajil"]), js([]),
     "Baya selected as fire in perception frame; common-noun A ERG.", "0.84"),
    (23, "The man saw the smoke at camp.",
     "Dingkar-angka kubu bayan-ba nyajin.",
     "dingkar-angka kubu bayan-ba nyaji-ny | man-ERG smoke camp-LOC see-PAST",
     "r140_perception_man_smoke_camp", 4, js(["dingkar", "kubu", "bayan", "nyajil"]), js([]),
     "Kubu smoke in perception frame; common-noun A ERG.", "0.84"),
    (24, "Mother saw the grass tree.",
     "Ngamu-ngku nganjirr nyajin.",
     "ngamu-ngku nganjirr nyaji-ny | mother-ERG grass.tree see-PAST",
     "r140_perception_mother_grass_tree", 4, js(["ngamu", "nganjirr", "nyajil"]), js([]),
     "Nganjirr grass tree in perception frame only.", "0.84"),
    (25, "They saw the smoke.",
     "Jana kubu nyajin.",
     "jana kubu nyaji-ny | they smoke see-PAST",
     "r140_perception_they_smoke", 4, js(["jana", "kubu", "nyajil"]), js([]),
     "Pronoun A bare; kubu smoke selected sense.", "0.84"),
    (26, "We saw the fire.",
     "Ngana baya nyajin.",
     "ngana baya nyaji-ny | we fire see-PAST",
     "r140_perception_we_fire", 4, js(["ngana", "baya", "nyajil"]), js([]),
     "Pronoun A bare; baya selected as fire.", "0.84"),
    (27, "Grandmother saw the grass tree.",
     "Babi-ngka nganjirr nyajin.",
     "babi-ngka nganjirr nyaji-ny | grandmother-ERG grass.tree see-PAST",
     "r140_perception_grandmother_grass_tree", 4, js(["babi", "nganjirr", "nyajil"]), js([]),
     "Common-noun A ERG; nganjirr selected as grass tree.", "0.84"),
    (28, "The child saw the smoke.",
     "Karrkay-ngku kubu nyajin.",
     "karrkay-ngku kubu nyaji-ny | child-ERG smoke see-PAST",
     "r140_perception_child_smoke", 4, js(["karrkay", "kubu", "nyajil"]), js([]),
     "Common-noun A ERG; kubu smoke.", "0.84"),

    (29, "Mother put the fire stick at camp.",
     "Ngamu-ngku jimal bayan-ba yijarrin.",
     "ngamu-ngku jimal bayan-ba yijarri-ny | mother-ERG fire.stick camp-LOC put-PAST",
     "r140_place_mother_fire_stick_camp", 4, js(["ngamu", "jimal", "bayan", "yijarril"]), js([]),
     "Yijarri-l put/place with jimal fire stick and bayan-ba support LOC.", "0.84"),
    (30, "Father put the charcoal in the shade.",
     "Nganjan-angka ngunjil wabarr-ba yijarrin.",
     "nganjan-angka ngunjil wabarr-ba yijarri-ny | father-ERG charcoal shade-LOC put-PAST",
     "r140_place_father_charcoal_shade", 4, js(["nganjan", "ngunjil", "wabarr", "yijarril"]), js([]),
     "Ngunjil charcoal in placement frame; no ash-residue head.", "0.82"),
    (31, "Grandmother put the bark outside.",
     "Babi-ngka dumbul ngalkal-ba yijarrin.",
     "babi-ngka dumbul ngalkal-ba yijarri-ny | grandmother-ERG bark outside-LOC put-PAST",
     "r140_place_grandmother_bark_outside", 4, js(["babi", "dumbul", "ngalkal", "yijarril"]), js([]),
     "Dumbul bark object plus ngalkal-ba outside LOC.", "0.84"),
    (32, "The child put the paperbark at camp.",
     "Karrkay-ngku buyku bayan-ba yijarrin.",
     "karrkay-ngku buyku bayan-ba yijarri-ny | child-ERG paperbark camp-LOC put-PAST",
     "r140_place_child_paperbark_camp", 4, js(["karrkay", "buyku", "bayan", "yijarril"]), js([]),
     "Buyku paperbark object with bayan-ba support LOC.", "0.84"),
    (33, "Grandfather put the grass outside.",
     "Kami-ngka jikan ngalkal-ba yijarrin.",
     "kami-ngka jikan ngalkal-ba yijarri-ny | grandfather-ERG grass outside-LOC put-PAST",
     "r140_place_grandfather_grass_outside", 4, js(["kami", "jikan", "ngalkal", "yijarril"]), js([]),
     "Jikan grass object; no target case morphology.", "0.82"),
    (34, "The woman put the fire stick outside.",
     "Jalbu-ngku jimal ngalkal-ba yijarrin.",
     "jalbu-ngku jimal ngalkal-ba yijarri-ny | woman-ERG fire.stick outside-LOC put-PAST",
     "r140_place_woman_fire_stick_outside", 4, js(["jalbu", "jimal", "ngalkal", "yijarril"]), js([]),
     "Jimal fire stick with source-backed ngalkal-ba outside LOC.", "0.84"),
    (35, "The man put the bark in the bag.",
     "Dingkar-angka dumbul balji-nga yijarrin.",
     "dingkar-angka dumbul balji-nga yijarri-ny | man-ERG bark bag-LOC put-PAST",
     "r140_place_man_bark_bag", 4, js(["dingkar", "dumbul", "balji", "yijarril"]), js([]),
     "Dumbul bark object; balji-nga support-only bag LOC.", "0.84"),
    (36, "I put the paperbark in the shade.",
     "Ngayu buyku wabarr-ba yijarrin.",
     "ngayu buyku wabarr-ba yijarri-ny | I paperbark shade-LOC put-PAST",
     "r140_place_i_paperbark_shade", 4, js(["ngayu", "buyku", "wabarr", "yijarril"]), js([]),
     "Pronoun A bare; buyku paperbark object plus shade LOC.", "0.84"),

    (37, "Mother left the fire stick at camp.",
     "Ngamu-ngku jimal bayan-ba bawan.",
     "ngamu-ngku jimal bayan-ba bawa-ny | mother-ERG fire.stick camp-LOC leave-PAST",
     "r140_leave_mother_fire_stick_camp", 4, js(["ngamu", "jimal", "bayan", "bawal"]), js([]),
     "Bawa-l leave with jimal fire stick and bayan-ba support.", "0.84"),
    (38, "Father left the charcoal in the shade.",
     "Nganjan-angka ngunjil wabarr-ba bawan.",
     "nganjan-angka ngunjil wabarr-ba bawa-ny | father-ERG charcoal shade-LOC leave-PAST",
     "r140_leave_father_charcoal_shade", 4, js(["nganjan", "ngunjil", "wabarr", "bawal"]), js([]),
     "Ngunjil charcoal in leave-location frame.", "0.82"),
    (39, "Grandmother left the bark outside.",
     "Babi-ngka dumbul ngalkal-ba bawan.",
     "babi-ngka dumbul ngalkal-ba bawa-ny | grandmother-ERG bark outside-LOC leave-PAST",
     "r140_leave_grandmother_bark_outside", 4, js(["babi", "dumbul", "ngalkal", "bawal"]), js([]),
     "Dumbul bark with ngalkal-ba outside LOC.", "0.84"),
    (40, "The child left the paperbark at camp.",
     "Karrkay-ngku buyku bayan-ba bawan.",
     "karrkay-ngku buyku bayan-ba bawa-ny | child-ERG paperbark camp-LOC leave-PAST",
     "r140_leave_child_paperbark_camp", 4, js(["karrkay", "buyku", "bayan", "bawal"]), js([]),
     "Buyku paperbark object plus camp LOC.", "0.84"),
    (41, "Grandfather left the grass outside.",
     "Kami-ngka jikan ngalkal-ba bawan.",
     "kami-ngka jikan ngalkal-ba bawa-ny | grandfather-ERG grass outside-LOC leave-PAST",
     "r140_leave_grandfather_grass_outside", 4, js(["kami", "jikan", "ngalkal", "bawal"]), js([]),
     "Jikan grass object remains bare; ngalkal-ba is support LOC.", "0.82"),
    (42, "The woman left the fire stick on the ground.",
     "Jalbu-ngku jimal bubu-ngu bawan.",
     "jalbu-ngku jimal bubu-ngu bawa-ny | woman-ERG fire.stick ground-LOC leave-PAST",
     "r140_leave_woman_fire_stick_ground", 4, js(["jalbu", "jimal", "bubu", "bawal"]), js([]),
     "Jimal fire stick object with bubu-ngu ground LOC.", "0.84"),
    (43, "The man left the charcoal in the bag.",
     "Dingkar-angka ngunjil balji-nga bawan.",
     "dingkar-angka ngunjil balji-nga bawa-ny | man-ERG charcoal bag-LOC leave-PAST",
     "r140_leave_man_charcoal_bag", 4, js(["dingkar", "ngunjil", "balji", "bawal"]), js([]),
     "Ngunjil charcoal with balji-nga support LOC.", "0.82"),
    (44, "I left the paperbark in the shade.",
     "Ngayu buyku wabarr-ba bawan.",
     "ngayu buyku wabarr-ba bawa-ny | I paperbark shade-LOC leave-PAST",
     "r140_leave_i_paperbark_shade", 4, js(["ngayu", "buyku", "wabarr", "bawal"]), js([]),
     "Pronoun A bare; buyku paperbark object plus shade LOC.", "0.84"),

    (45, "The woman took the fire stick from the ground.",
     "Jalbu-ngku jimal bubu-muny manin.",
     "jalbu-ngku jimal bubu-muny mani-ny | woman-ERG fire.stick ground-ABL get-PAST",
     "r140_get_woman_fire_stick_ground", 4, js(["jalbu", "jimal", "bubu", "manil"]), js([]),
     "Mani-l get/take with jimal fire stick and bubu-muny source.", "0.84"),
    (46, "The man took the bark from the ground.",
     "Dingkar-angka dumbul bubu-muny manin.",
     "dingkar-angka dumbul bubu-muny mani-ny | man-ERG bark ground-ABL get-PAST",
     "r140_get_man_bark_ground", 4, js(["dingkar", "dumbul", "bubu", "manil"]), js([]),
     "Dumbul bark object with bubu-muny source.", "0.84"),
    (47, "Grandfather took the paperbark from the ground.",
     "Kami-ngka buyku bubu-muny manin.",
     "kami-ngka buyku bubu-muny mani-ny | grandfather-ERG paperbark ground-ABL get-PAST",
     "r140_get_grandfather_paperbark_ground", 4, js(["kami", "buyku", "bubu", "manil"]), js([]),
     "Buyku paperbark object with bubu-muny source.", "0.84"),
    (48, "Mother took the grass from the ground.",
     "Ngamu-ngku jikan bubu-muny manin.",
     "ngamu-ngku jikan bubu-muny mani-ny | mother-ERG grass ground-ABL get-PAST",
     "r140_get_mother_grass_ground", 4, js(["ngamu", "jikan", "bubu", "manil"]), js([]),
     "Jikan grass object with bubu-muny source.", "0.82"),
    (49, "Father took the charcoal from the ground.",
     "Nganjan-angka ngunjil bubu-muny manin.",
     "nganjan-angka ngunjil bubu-muny mani-ny | father-ERG charcoal ground-ABL get-PAST",
     "r140_get_father_charcoal_ground", 4, js(["nganjan", "ngunjil", "bubu", "manil"]), js([]),
     "Ngunjil charcoal object with bubu-muny source; no bajar residue-ash row.", "0.82"),
    (50, "We took the fire stick from the ground.",
     "Ngana jimal bubu-muny manin.",
     "ngana jimal bubu-muny mani-ny | we fire.stick ground-ABL get-PAST",
     "r140_get_we_fire_stick_ground", 4, js(["ngana", "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("r140_"):
            issues.append(f"{label}: Rule 140 frame must start with r140_")
        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 140 construction 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 140")
        if "COMIT" in analysis:
            issues.append(f"{label}: no comitative rows in Rule 140")
        if re.search(r"-(?:amu|mu|ndamun|andamun)\b", low):
            issues.append(f"{label}: no alienable possessive suffixes in Rule 140")
        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 140 row must exercise a Rule 139 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, "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 140")
        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, "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 140 draft: construction consolidation after Rule 139", "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 140 source pass retained the Rule 139 targets jimal, ngunjil, dumbul, buyku, jikan, baya, kubu, "
            "and nganjirr, plus support forms balji-nga, bubu-ngu, bubu-muny, wabarr-ba, bayan-ba, and source-backed "
            "ngalkal-ba outside LOC. Bajar remains rejected as residue ash; ngalku grassfire remains unopened."
        ),
        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="construction-scope",
        actor="claude-main",
        detail=(
            "Rule 140 admits no new target heads. It consolidates Rule 139 camp/fire-material heads in state, "
            "rest-location, perception, placement, leave, and get/source frames. Baya, kubu, and nganjirr remain "
            "restricted to state/perception."
        ),
        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.",
        command="sqlite exact-surface query over planned Batch 182 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 182 stores source readback, construction-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 140 construction-consolidation rows as under_review after static and exact duplicate checks.",
        command="python3 batch_scripts/bank_182.py",
        output_excerpt="banked batch-182: 50 rows under_review; duplicate scan clean",
        artifact_path="batch_scripts/bank_182.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())
