#!/usr/bin/env python3
"""Bank batch-180: Rule 138 construction consolidation after Rule 137."""

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

TARGET_HEADS = {
    "badur": "hook.line",
    "karra": "rope",
    "makarr": "fish.net",
    "ngunyin": "dilly.bag",
    "wangal": "boomerang",
    "warrbi": "axe",
    "kunjuri": "shield",
    "kalka": "spear",
}
STATE_HEADS = {
    "ngulkurr": "good",
    "marral": "dry",
}
SUPPORT_HEADS = {
    "balji": "bag",
    "bubu": "ground",
    "wabarr": "shade",
    "bayan": "camp",
}
APPROVED_LOCAL_FORMS = {"balji-nga", "bubu-ngu", "wabarr-ba", "bayan-ba", "bubu-muny"}
DENY = {
    "dakil",
    "waba",
    "jandal",
    "jibarr",
    "malbin",
    "jiba",
    "bangkarr",
    "minya",
    "jungkalu",
    "juku",
    "kulji",
    "kambi",
    "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 hook and line is dry.",
     "Badur marral.",
     "badur marral | hook.line dry",
     "r138_state_hook_line_dry", 2, js(["badur", "marral"]), js([]),
     "Rule 138 consolidation of dictionary badur hook/line with already verified marral dry.", "0.86"),
    (2, "The rope is good.",
     "Karra ngulkurr.",
     "karra ngulkurr | rope good",
     "r138_state_rope_good", 2, js(["karra", "ngulkurr"]), js([]),
     "Rule 138 consolidation of dictionary karra rope/cord/string with ngulkurr good.", "0.86"),
    (3, "The fish net is dry.",
     "Makarr marral.",
     "makarr marral | fish.net dry",
     "r138_state_fish_net_dry", 2, js(["makarr", "marral"]), js([]),
     "Rule 138 consolidation of makarr fish net with marral dry.", "0.86"),
    (4, "The dilly bag is good.",
     "Ngunyin ngulkurr.",
     "ngunyin ngulkurr | dilly.bag good",
     "r138_state_dilly_bag_good", 2, js(["ngunyin", "ngulkurr"]), js([]),
     "Rule 138 keeps ngunyin in the dilly-bag sense only and uses ngulkurr good.", "0.84"),
    (5, "The boomerang is good.",
     "Wangal ngulkurr.",
     "wangal ngulkurr | boomerang good",
     "r138_state_boomerang_good", 2, js(["wangal", "ngulkurr"]), js([]),
     "Rule 138 keeps wangal as boomerang noun only and uses ngulkurr good.", "0.86"),
    (6, "The axe is dry.",
     "Warrbi marral.",
     "warrbi marral | axe dry",
     "r138_state_axe_dry", 2, js(["warrbi", "marral"]), js([]),
     "Rule 138 keeps warrbi in the axe sense only and uses marral dry.", "0.84"),
    (7, "The shield is good.",
     "Kunjuri ngulkurr.",
     "kunjuri ngulkurr | shield good",
     "r138_state_shield_good", 2, js(["kunjuri", "ngulkurr"]), js([]),
     "Rule 138 consolidation of kunjuri shield with ngulkurr good.", "0.86"),
    (8, "The spear is good.",
     "Kalka ngulkurr.",
     "kalka ngulkurr | spear good",
     "r138_state_spear_good", 2, js(["kalka", "ngulkurr"]), js([]),
     "Rule 138 consolidation of kalka spear/fishing spear with ngulkurr good.", "0.86"),

    (9, "The hook and line lay on the ground.",
     "Badur bubu-ngu wunan.",
     "badur bubu-ngu wuna-ny | hook.line ground-LOC lie-PAST",
     "r138_rest_hook_line_ground", 3, js(["badur", "bubu", "wunay"]), js([]),
     "Rule 138 rest-location row using wuna-y and source-backed bubu-ngu ground LOC.", "0.84"),
    (10, "The fish net lay in the bag.",
     "Makarr balji-nga wunan.",
     "makarr balji-nga wuna-ny | fish.net bag-LOC lie-PAST",
     "r138_rest_fish_net_bag", 3, js(["makarr", "balji", "wunay"]), js([]),
     "Rule 138 rest-location row using Patz-sourced balji-nga as support-only bag LOC.", "0.84"),
    (11, "The dilly bag lay at camp.",
     "Ngunyin bayan-ba wunan.",
     "ngunyin bayan-ba wuna-ny | dilly.bag camp-LOC lie-PAST",
     "r138_rest_dilly_bag_camp", 3, js(["ngunyin", "bayan", "wunay"]), js([]),
     "Ngunyin selected as dilly bag only; bayan-ba camp/house LOC is controlled support.", "0.84"),
    (12, "The boomerang lay at camp.",
     "Wangal bayan-ba wunan.",
     "wangal bayan-ba wuna-ny | boomerang camp-LOC lie-PAST",
     "r138_rest_boomerang_camp", 3, js(["wangal", "bayan", "wunay"]), js([]),
     "Wangal boomerang noun in rest-location frame with bayan-ba support.", "0.84"),
    (13, "The axe lay in the bag.",
     "Warrbi balji-nga wunan.",
     "warrbi balji-nga wuna-ny | axe bag-LOC lie-PAST",
     "r138_rest_axe_bag", 3, js(["warrbi", "balji", "wunay"]), js([]),
     "Warrbi selected as axe only; balji-nga support LOC does not make it an instrument.", "0.84"),
    (14, "The shield lay in the shade.",
     "Kunjuri wabarr-ba wunan.",
     "kunjuri wabarr-ba wuna-ny | shield shade-LOC lie-PAST",
     "r138_rest_shield_shade", 3, js(["kunjuri", "wabarr", "wunay"]), js([]),
     "Kunjuri shield rest row with source-backed wabarr-ba shade LOC.", "0.84"),
    (15, "The spear lay on the ground.",
     "Kalka bubu-ngu wunan.",
     "kalka bubu-ngu wuna-ny | spear ground-LOC lie-PAST",
     "r138_rest_spear_ground", 3, js(["kalka", "bubu", "wunay"]), js([]),
     "Kalka spear rest row with bubu-ngu; no kalka case form.", "0.84"),
    (16, "The hook and line lay in the bag.",
     "Badur balji-nga wunan.",
     "badur balji-nga wuna-ny | hook.line bag-LOC lie-PAST",
     "r138_rest_hook_line_bag", 3, js(["badur", "balji", "wunay"]), js([]),
     "Badur hook/line as rest subject with balji-nga support LOC.", "0.84"),

    (17, "Mother saw the rope in the bag.",
     "Ngamu-ngku karra balji-nga nyajin.",
     "ngamu-ngku karra balji-nga nyaji-ny | mother-ERG rope bag-LOC see-PAST",
     "r138_perception_mother_rope_bag", 4, js(["ngamu", "karra", "balji", "nyajil"]), js([]),
     "Common-noun A ERG; karra rope object plus balji-nga support LOC.", "0.84"),
    (18, "Father saw the fish net in the bag.",
     "Nganjan-angka makarr balji-nga nyajin.",
     "nganjan-angka makarr balji-nga nyaji-ny | father-ERG fish.net bag-LOC see-PAST",
     "r138_perception_father_fish_net_bag", 4, js(["nganjan", "makarr", "balji", "nyajil"]), js([]),
     "Common-noun A ERG; makarr fish net object plus balji-nga support LOC.", "0.84"),
    (19, "Grandmother saw the dilly bag at camp.",
     "Babi-ngka ngunyin bayan-ba nyajin.",
     "babi-ngka ngunyin bayan-ba nyaji-ny | grandmother-ERG dilly.bag camp-LOC see-PAST",
     "r138_perception_grandmother_dilly_bag_camp", 4, js(["babi", "ngunyin", "bayan", "nyajil"]), js([]),
     "Common-noun A ERG; ngunyin dilly-bag sense only.", "0.84"),
    (20, "The child saw the boomerang in the bag.",
     "Karrkay-ngku wangal balji-nga nyajin.",
     "karrkay-ngku wangal balji-nga nyaji-ny | child-ERG boomerang bag-LOC see-PAST",
     "r138_perception_child_boomerang_bag", 4, js(["karrkay", "wangal", "balji", "nyajil"]), js([]),
     "Common-noun A ERG; wangal boomerang noun only.", "0.84"),
    (21, "Grandfather saw the shield in the bag.",
     "Kami-ngka kunjuri balji-nga nyajin.",
     "kami-ngka kunjuri balji-nga nyaji-ny | grandfather-ERG shield bag-LOC see-PAST",
     "r138_perception_grandfather_shield_bag", 4, js(["kami", "kunjuri", "balji", "nyajil"]), js([]),
     "Common-noun A ERG; kunjuri shield object remains bare.", "0.84"),
    (22, "The woman saw the spear in the shade.",
     "Jalbu-ngku kalka wabarr-ba nyajin.",
     "jalbu-ngku kalka wabarr-ba nyaji-ny | woman-ERG spear shade-LOC see-PAST",
     "r138_perception_woman_spear_shade", 4, js(["jalbu", "kalka", "wabarr", "nyajil"]), js([]),
     "Common-noun A ERG; kalka spear object plus shade LOC.", "0.84"),
    (23, "The man saw the axe in the shade.",
     "Dingkar-angka warrbi wabarr-ba nyajin.",
     "dingkar-angka warrbi wabarr-ba nyaji-ny | man-ERG axe shade-LOC see-PAST",
     "r138_perception_man_axe_shade", 4, js(["dingkar", "warrbi", "wabarr", "nyajil"]), js([]),
     "Common-noun A ERG; warrbi axe sense only.", "0.84"),
    (24, "I saw the rope at camp.",
     "Ngayu karra bayan-ba nyajin.",
     "ngayu karra bayan-ba nyaji-ny | I rope camp-LOC see-PAST",
     "r138_perception_pronoun_rope_camp", 4, js(["ngayu", "karra", "bayan", "nyajil"]), js([]),
     "Pronoun A bare; karra rope object with bayan-ba support LOC.", "0.84"),
    (25, "They saw the axe in the bag.",
     "Jana warrbi balji-nga nyajin.",
     "jana warrbi balji-nga nyaji-ny | they axe bag-LOC see-PAST",
     "r138_perception_they_axe_bag", 4, js(["jana", "warrbi", "balji", "nyajil"]), js([]),
     "Pronoun A bare; warrbi axe object with bag LOC.", "0.84"),
    (26, "We saw the shield in the shade.",
     "Ngana kunjuri wabarr-ba nyajin.",
     "ngana kunjuri wabarr-ba nyaji-ny | we shield shade-LOC see-PAST",
     "r138_perception_we_shield_shade", 4, js(["ngana", "kunjuri", "wabarr", "nyajil"]), js([]),
     "Pronoun A bare; kunjuri shield object with shade LOC.", "0.84"),
    (27, "Mother saw the boomerang at camp.",
     "Ngamu-ngku wangal bayan-ba nyajin.",
     "ngamu-ngku wangal bayan-ba nyaji-ny | mother-ERG boomerang camp-LOC see-PAST",
     "r138_perception_mother_boomerang_camp", 4, js(["ngamu", "wangal", "bayan", "nyajil"]), js([]),
     "Common-noun A ERG; wangal boomerang noun only.", "0.84"),
    (28, "Father saw the hook and line on the ground.",
     "Nganjan-angka badur bubu-ngu nyajin.",
     "nganjan-angka badur bubu-ngu nyaji-ny | father-ERG hook.line ground-LOC see-PAST",
     "r138_perception_father_hook_line_ground", 4, js(["nganjan", "badur", "bubu", "nyajil"]), js([]),
     "Common-noun A ERG; badur hook/line object plus ground LOC.", "0.84"),

    (29, "Grandmother put the hook and line on the ground.",
     "Babi-ngka badur bubu-ngu yijarrin.",
     "babi-ngka badur bubu-ngu yijarri-ny | grandmother-ERG hook.line ground-LOC put-PAST",
     "r138_place_grandmother_hook_line_ground", 4, js(["babi", "badur", "bubu", "yijarril"]), js([]),
     "Yijarri-l put/place with common-noun A ERG and badur bare object.", "0.84"),
    (30, "The child put the rope in the bag.",
     "Karrkay-ngku karra balji-nga yijarrin.",
     "karrkay-ngku karra balji-nga yijarri-ny | child-ERG rope bag-LOC put-PAST",
     "r138_place_child_rope_bag", 4, js(["karrkay", "karra", "balji", "yijarril"]), js([]),
     "Yijarri-l put/place with karra rope and balji-nga support LOC.", "0.84"),
    (31, "Grandfather put the fish net in the bag.",
     "Kami-ngka makarr balji-nga yijarrin.",
     "kami-ngka makarr balji-nga yijarri-ny | grandfather-ERG fish.net bag-LOC put-PAST",
     "r138_place_grandfather_fish_net_bag", 4, js(["kami", "makarr", "balji", "yijarril"]), js([]),
     "Yijarri-l put/place with makarr fish net and bag LOC.", "0.84"),
    (32, "The woman put the dilly bag at camp.",
     "Jalbu-ngku ngunyin bayan-ba yijarrin.",
     "jalbu-ngku ngunyin bayan-ba yijarri-ny | woman-ERG dilly.bag camp-LOC put-PAST",
     "r138_place_woman_dilly_bag_camp", 4, js(["jalbu", "ngunyin", "bayan", "yijarril"]), js([]),
     "Ngunyin selected as dilly bag only; yijarri-l plus bayan-ba support.", "0.84"),
    (33, "The man put the boomerang in the bag.",
     "Dingkar-angka wangal balji-nga yijarrin.",
     "dingkar-angka wangal balji-nga yijarri-ny | man-ERG boomerang bag-LOC put-PAST",
     "r138_place_man_boomerang_bag", 4, js(["dingkar", "wangal", "balji", "yijarril"]), js([]),
     "Wangal boomerang noun only; common-noun A ERG.", "0.84"),
    (34, "Mother put the axe in the bag.",
     "Ngamu-ngku warrbi balji-nga yijarrin.",
     "ngamu-ngku warrbi balji-nga yijarri-ny | mother-ERG axe bag-LOC put-PAST",
     "r138_place_mother_axe_bag", 4, js(["ngamu", "warrbi", "balji", "yijarril"]), js([]),
     "Warrbi axe object with balji-nga support; no instrument morphology.", "0.84"),
    (35, "Father put the shield on the ground.",
     "Nganjan-angka kunjuri bubu-ngu yijarrin.",
     "nganjan-angka kunjuri bubu-ngu yijarri-ny | father-ERG shield ground-LOC put-PAST",
     "r138_place_father_shield_ground", 4, js(["nganjan", "kunjuri", "bubu", "yijarril"]), js([]),
     "Kunjuri shield bare object; common-noun A ERG.", "0.84"),
    (36, "Grandmother put the spear in the shade.",
     "Babi-ngka kalka wabarr-ba yijarrin.",
     "babi-ngka kalka wabarr-ba yijarri-ny | grandmother-ERG spear shade-LOC put-PAST",
     "r138_place_grandmother_spear_shade", 4, js(["babi", "kalka", "wabarr", "yijarril"]), js([]),
     "Kalka spear bare object with shade LOC; no kalka case form.", "0.84"),
    (37, "I put the rope in the shade.",
     "Ngayu karra wabarr-ba yijarrin.",
     "ngayu karra wabarr-ba yijarri-ny | I rope shade-LOC put-PAST",
     "r138_place_pronoun_rope_shade", 4, js(["ngayu", "karra", "wabarr", "yijarril"]), js([]),
     "Pronoun A bare; karra rope object plus shade LOC.", "0.84"),
    (38, "They put the axe at camp.",
     "Jana warrbi bayan-ba yijarrin.",
     "jana warrbi bayan-ba yijarri-ny | they axe camp-LOC put-PAST",
     "r138_place_they_axe_camp", 4, js(["jana", "warrbi", "bayan", "yijarril"]), js([]),
     "Pronoun A bare; warrbi axe object plus camp LOC.", "0.84"),

    (39, "Mother left the rope in the bag.",
     "Ngamu-ngku karra balji-nga bawan.",
     "ngamu-ngku karra balji-nga bawa-ny | mother-ERG rope bag-LOC leave-PAST",
     "r138_leave_mother_rope_bag", 4, js(["ngamu", "karra", "balji", "bawal"]), js([]),
     "Bawa-l leave with common-noun A ERG and karra bare object.", "0.84"),
    (40, "Father left the fish net on the ground.",
     "Nganjan-angka makarr bubu-ngu bawan.",
     "nganjan-angka makarr bubu-ngu bawa-ny | father-ERG fish.net ground-LOC leave-PAST",
     "r138_leave_father_fish_net_ground", 4, js(["nganjan", "makarr", "bubu", "bawal"]), js([]),
     "Bawa-l leave with makarr fish net and bubu-ngu support.", "0.84"),
    (41, "Grandmother left the boomerang in the bag.",
     "Babi-ngka wangal balji-nga bawan.",
     "babi-ngka wangal balji-nga bawa-ny | grandmother-ERG boomerang bag-LOC leave-PAST",
     "r138_leave_grandmother_boomerang_bag", 4, js(["babi", "wangal", "balji", "bawal"]), js([]),
     "Wangal boomerang noun only; bawa-l leave with bag LOC.", "0.84"),
    (42, "The child left the axe in the bag.",
     "Karrkay-ngku warrbi balji-nga bawan.",
     "karrkay-ngku warrbi balji-nga bawa-ny | child-ERG axe bag-LOC leave-PAST",
     "r138_leave_child_axe_bag", 4, js(["karrkay", "warrbi", "balji", "bawal"]), js([]),
     "Warrbi axe object; no instrument morphology.", "0.84"),
    (43, "Grandfather left the shield in the bag.",
     "Kami-ngka kunjuri balji-nga bawan.",
     "kami-ngka kunjuri balji-nga bawa-ny | grandfather-ERG shield bag-LOC leave-PAST",
     "r138_leave_grandfather_shield_bag", 4, js(["kami", "kunjuri", "balji", "bawal"]), js([]),
     "Kunjuri shield bare object with balji-nga support LOC.", "0.84"),
    (44, "The woman left the spear on the ground.",
     "Jalbu-ngku kalka bubu-ngu bawan.",
     "jalbu-ngku kalka bubu-ngu bawa-ny | woman-ERG spear ground-LOC leave-PAST",
     "r138_leave_woman_spear_ground", 4, js(["jalbu", "kalka", "bubu", "bawal"]), js([]),
     "Kalka spear object plus ground LOC; no kalka case form.", "0.84"),
    (45, "The man left the hook and line at camp.",
     "Dingkar-angka badur bayan-ba bawan.",
     "dingkar-angka badur bayan-ba bawa-ny | man-ERG hook.line camp-LOC leave-PAST",
     "r138_leave_man_hook_line_camp", 4, js(["dingkar", "badur", "bayan", "bawal"]), js([]),
     "Badur hook/line object with camp LOC; common-noun A ERG.", "0.84"),
    (46, "I left the fish net in the shade.",
     "Ngayu makarr wabarr-ba bawan.",
     "ngayu makarr wabarr-ba bawa-ny | I fish.net shade-LOC leave-PAST",
     "r138_leave_pronoun_fish_net_shade", 4, js(["ngayu", "makarr", "wabarr", "bawal"]), js([]),
     "Pronoun A bare; makarr fish net object with shade LOC.", "0.84"),

    (47, "Mother took the boomerang from the ground.",
     "Ngamu-ngku wangal bubu-muny manin.",
     "ngamu-ngku wangal bubu-muny mani-ny | mother-ERG boomerang ground-ABL get-PAST",
     "r138_get_mother_boomerang_ground", 4, js(["ngamu", "wangal", "bubu", "manil"]), js([]),
     "Mani-l get/take with bubu-muny source and common-noun A ERG.", "0.84"),
    (48, "Father took the spear from the ground.",
     "Nganjan-angka kalka bubu-muny manin.",
     "nganjan-angka kalka bubu-muny mani-ny | father-ERG spear ground-ABL get-PAST",
     "r138_get_father_spear_ground", 4, js(["nganjan", "kalka", "bubu", "manil"]), js([]),
     "Kalka spear object with bubu-muny source; no target case form.", "0.84"),
    (49, "Grandmother took the axe from the ground.",
     "Babi-ngka warrbi bubu-muny manin.",
     "babi-ngka warrbi bubu-muny mani-ny | grandmother-ERG axe ground-ABL get-PAST",
     "r138_get_grandmother_axe_ground", 4, js(["babi", "warrbi", "bubu", "manil"]), js([]),
     "Warrbi axe object with mani-l get/take and ground ABL.", "0.84"),
    (50, "We took the hook and line from the ground.",
     "Ngana badur bubu-muny manin.",
     "ngana badur bubu-muny mani-ny | we hook.line ground-ABL get-PAST",
     "r138_get_we_hook_line_ground", 4, js(["ngana", "badur", "bubu", "manil"]), js([]),
     "Pronoun A bare; badur hook/line object with 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("r138_"):
            issues.append(f"{label}: Rule 138 frame must start with r138_")
        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 138 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 138")
        if "COMIT" in analysis:
            issues.append(f"{label}: no comitative rows in Rule 138")
        if re.search(r"-(?:amu|mu|ndamun|andamun)\b", low):
            issues.append(f"{label}: no alienable possessive suffixes in Rule 138")
        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 138 row must exercise a Rule 137 handled-object 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 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, "warrbi") and ("house" in english_low or "house" in analysis_low):
            issues.append(f"{label}: warrbi must stay axe only, not grass-house")
        if has_base(low, "ngunyin") and any(bad in english_low or bad in analysis_low for bad in ("bark", "spider", "placenta")):
            issues.append(f"{label}: ngunyin must stay dilly-bag only")
        if has_base(low, "wangal") and ("dance" in english_low or "dance" in analysis_low):
            issues.append(f"{label}: wangal must stay boomerang noun only")
        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 138 draft: construction consolidation after Rule 137", "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 138 source pass rechecked the Rule 137 handled-object targets and retained Patz grammar anchors "
            "for state predicates, wuna-y rest/location, optional locative NPs, balji-nga bag LOC, bubu-ngu ground "
            "LOC, bubu-muny ground ABL, wabarr-ba shade LOC, bayan-ba camp/house LOC, nyaji-l see/perceive, "
            "yijarri-l put/place, bawa-l leave, and mani-l get/take."
        ),
        command="sed/rg 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 138 admits no new target heads. It consolidates badur, karra, makarr, ngunyin, wangal, warrbi, "
            "kunjuri, and kalka in state, rest-location, perception, placement, leave, and get/source frames."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="scope",
        event_type="risk-controls",
        actor="claude-main",
        detail=(
            "Controls: balji remains support-only; no target-head case forms; no instruments, comitatives, "
            "possessives, temporal clitics, or unsourced analogies; no waba, jandal, dakil, locational jibarr, "
            "malbin, jiba, bangkarr, minya, jungkalu, juku, kulji, or kambi; warrbi axe only, ngunyin dilly bag "
            "only, wangal boomerang noun only."
        ),
        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 180 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 180 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 138 construction-consolidation rows as under_review after static and exact duplicate checks.",
        command="python3 batch_scripts/bank_180.py",
        output_excerpt="banked batch-180: 50 rows under_review; duplicate scan clean",
        artifact_path="batch_scripts/bank_180.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())
