#!/usr/bin/env python3
"""Bank batch-170: Rule 128 case-frame consolidation after Rule 127."""

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

DENY = {"yah", "ngawuya", "walnga", "maja-nga", "yunga", "bada-bada", "wangkar-wangkar", "biyul"}
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")
NO_NEW_INSTRUMENT_HEADS = ("juku", "jarba", "bilngkumu", "yirku-yirku", "buyi")


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


ROWS = [
    (1, "The water lay at camp.",
     "Bana bayan-ba wunan.",
     "bana bayan-ba wuna-ny | water camp-LOC lie-PAST",
     "r128_rest_water_camp", 3, js(["bana", "bayan", "wunay"]), js([]),
     "Bana water with retained bayan-ba camp LOC.", "0.84"),
    (2, "The firewood lay on the ground.",
     "Baya bubu-ngu wunan.",
     "baya bubu-ngu wuna-ny | firewood ground-LOC lie-PAST",
     "r128_rest_firewood_ground", 3, js(["baya", "bubu", "wunay"]), js([]),
     "Baya fire/firewood; possible English etymology noted but unsubstantiated.", "0.84"),
    (3, "The stick lay in the shade.",
     "Juku wabarr-ba wunan.",
     "juku wabarr-ba wuna-ny | stick shade-LOC lie-PAST",
     "r128_rest_stick_shade", 3, js(["juku", "wabarr", "wunay"]), js([]),
     "Juku stick/wood in a rest frame, not a new instrument.", "0.84"),
    (4, "The paperbark lay outside.",
     "Buyku ngalkal-ba wunan.",
     "buyku ngalkal-ba wuna-ny | paperbark outside-LOC lie-PAST",
     "r128_rest_paperbark_outside", 3, js(["buyku", "ngalkal", "wunay"]), js([]),
     "Buyku paperbark plus retained outside LOC.", "0.84"),
    (5, "The grass lay on the ground.",
     "Jikan bubu-ngu wunan.",
     "jikan bubu-ngu wuna-ny | grass ground-LOC lie-PAST",
     "r128_rest_grass_ground", 3, js(["jikan", "bubu", "wunay"]), js([]),
     "Jikan grass with retained ground LOC.", "0.84"),
    (6, "The humpy stands at camp.",
     "Wurun bayan-ba janay.",
     "wurun bayan-ba jana-y | humpy camp-LOC stand-NONPAST",
     "r128_stand_humpy_camp", 3, js(["wurun", "bayan", "janay"]), js([]),
     "Wurun humpy/shelter plus retained janay stand/be predicate.", "0.84"),
    (7, "The snake lay on the ground.",
     "Jarba bubu-ngu wunan.",
     "jarba bubu-ngu wuna-ny | snake ground-LOC lie-PAST",
     "r128_rest_snake_ground", 3, js(["jarba", "bubu", "wunay"]), js([]),
     "Jarba snake as rest subject only, not agent or instrument.", "0.84"),
    (8, "The crocodile lay outside.",
     "Bilngkumu ngalkal-ba wunan.",
     "bilngkumu ngalkal-ba wuna-ny | crocodile outside-LOC lie-PAST",
     "r128_rest_crocodile_outside", 3, js(["bilngkumu", "ngalkal", "wunay"]), js([]),
     "Bilngkumu crocodile as rest subject only.", "0.84"),
    (9, "The frog lay outside.",
     "Yirku-yirku ngalkal-ba wunan.",
     "yirku-yirku ngalkal-ba wuna-ny | frog outside-LOC lie-PAST",
     "r128_rest_frog_outside", 3, js(["yirku-yirku", "ngalkal", "wunay"]), js([]),
     "Yirku-yirku frog as rest subject only.", "0.84"),
    (10, "The bird is sitting in the tree.",
     "Buyi juku-ngu bundanday.",
     "buyi juku-ngu bunda-n-bunda-y | bird tree-LOC sit-REDUP-NONPAST",
     "r128_rest_bird_tree", 3, js(["buyi", "juku", "bunday"]), js([]),
     "Buyi bird plus sourced juku-ngu tree LOC.", "0.84"),

    (11, "The woman left the firewood in the shade.",
     "Jalbu-ngku baya wabarr-ba bawan.",
     "jalbu-ngku baya wabarr-ba bawa-ny | woman-ERG firewood shade-LOC leave-PAST",
     "r128_object_firewood_shade", 4, js(["jalbu", "baya", "wabarr", "bawal"]), js([]),
     "Baya object in retained leave-location frame.", "0.84"),
    (12, "The man put the stick on the ground.",
     "Dingkar-angka juku bubu-ngu yijarrin.",
     "dingkar-angka juku bubu-ngu yijarri-ny | man-ERG stick ground-LOC put-PAST",
     "r128_object_stick_ground", 4, js(["dingkar", "juku", "bubu", "yijarril"]), js([]),
     "Juku object in placement frame, not instrument.", "0.84"),
    (13, "Grandmother put the paperbark in the bag.",
     "Babi-ngka buyku balji-nga yijarrin.",
     "babi-ngka buyku balji-nga yijarri-ny | grandmother-ERG paperbark bag-LOC put-PAST",
     "r128_object_paperbark_bag", 4, js(["babi", "buyku", "balji", "yijarril"]), js([]),
     "Buyku object plus retained balji-nga.", "0.84"),
    (14, "Mother left the grass in the shade.",
     "Ngamu-ngku jikan wabarr-ba bawan.",
     "ngamu-ngku jikan wabarr-ba bawa-ny | mother-ERG grass shade-LOC leave-PAST",
     "r128_object_grass_shade", 4, js(["ngamu", "jikan", "wabarr", "bawal"]), js([]),
     "Jikan grass object in leave-location frame.", "0.84"),
    (15, "Mother took water to camp.",
     "Ngamu-ngku bana bayan-ka wundin.",
     "ngamu-ngku bana bayan-ka wundi-ny | mother-ERG water camp-DAT take-PAST",
     "r128_object_water_camp_goal", 4, js(["ngamu", "bana", "bayan", "wundil"]), js([]),
     "Bana object plus retained bayan-ka goal; common-noun A ERG.", "0.84"),
    (16, "Grandfather got firewood outside.",
     "Kami-ngka baya ngalkal-ba manin.",
     "kami-ngka baya ngalkal-ba mani-ny | grandfather-ERG firewood outside-LOC get-PAST",
     "r128_object_firewood_outside", 4, js(["kami", "baya", "ngalkal", "manil"]), js([]),
     "Baya object in get-at-place frame.", "0.84"),
    (17, "The old woman left the paperbark on the road.",
     "Wulbuman-angka buyku baral-ba bawan.",
     "wulbuman-angka buyku baral-ba bawa-ny | old.woman-ERG paperbark road-LOC leave-PAST",
     "r128_object_paperbark_road", 4, js(["wulbuman", "buyku", "baral", "bawal"]), js([]),
     "Buyku object plus retained baral-ba, no baral-muny.", "0.84"),
    (18, "I took the stick to camp.",
     "Ngayu juku bayan-ka wundin.",
     "ngayu juku bayan-ka wundi-ny | I stick camp-DAT take-PAST",
     "r128_object_stick_camp_goal", 4, js(["ngayu", "juku", "bayan", "wundil"]), js([]),
     "Pronoun A bare; juku stick object plus DAT goal.", "0.84"),
    (19, "We brought water from the house.",
     "Ngana bana bayan-muny wundin.",
     "ngana bana bayan-muny wundi-ny | we water house-ABL bring-PAST",
     "r128_object_water_house_source", 4, js(["ngana", "bana", "bayan", "wundil"]), js([]),
     "Pronoun A bare; bana object plus ABL source.", "0.84"),
    (20, "The man saw the humpy at camp.",
     "Dingkar-angka wurun bayan-ba nyajin.",
     "dingkar-angka wurun bayan-ba nyaji-ny | man-ERG humpy camp-LOC see-PAST",
     "r128_perception_man_humpy_camp", 4, js(["dingkar", "wurun", "bayan", "nyajil"]), js([]),
     "Wurun humpy object with ERG common-noun perceiver.", "0.84"),

    (21, "The child saw the cloud.",
     "Karrkay-ngku ngurrban nyajin.",
     "karrkay-ngku ngurrban nyaji-ny | child-ERG cloud see-PAST",
     "r128_perception_child_cloud", 4, js(["karrkay", "ngurrban", "nyajil"]), js([]),
     "Ngurrban cloud object; common-noun perceiver ERG.", "0.84"),
    (22, "I saw the cloud outside.",
     "Ngayu ngurrban ngalkal-ba nyajin.",
     "ngayu ngurrban ngalkal-ba nyaji-ny | I cloud outside-LOC see-PAST",
     "r128_perception_pronoun_cloud_outside", 4, js(["ngayu", "ngurrban", "ngalkal", "nyajil"]), js([]),
     "Pronoun A contrast for cloud perception.", "0.84"),
    (23, "Mother heard the wind.",
     "Ngamu-ngku muyar nyajin.",
     "ngamu-ngku muyar nyaji-ny | mother-ERG wind hear-PAST",
     "r128_perception_mother_wind", 4, js(["ngamu", "muyar", "nyajil"]), js([]),
     "Muyar wind heard with nyaji-l; common-noun A ERG.", "0.84"),
    (24, "We heard the wind outside.",
     "Ngana muyar ngalkal-ba nyajin.",
     "ngana muyar ngalkal-ba nyaji-ny | we wind outside-LOC hear-PAST",
     "r128_perception_pronoun_wind_outside", 4, js(["ngana", "muyar", "ngalkal", "nyajil"]), js([]),
     "Pronoun A bare; ngalkal-ba outside LOC retained.", "0.84"),
    (25, "The young man saw the sky.",
     "Warru-ngku jiri nyajin.",
     "warru-ngku jiri nyaji-ny | young.man-ERG sky see-PAST",
     "r128_perception_young_man_sky", 4, js(["warru", "jiri", "nyajil"]), js([]),
     "Jiri sky object with common-noun perceiver ERG.", "0.84"),
    (26, "We saw the sun.",
     "Ngana wungar nyajin.",
     "ngana wungar nyaji-ny | we sun see-PAST",
     "r128_perception_pronoun_sun", 4, js(["ngana", "wungar", "nyajil"]), js([]),
     "Wungar sun/day as perceived object.", "0.84"),
    (27, "Grandmother saw the bird outside.",
     "Babi-ngka buyi ngalkal-ba nyajin.",
     "babi-ngka buyi ngalkal-ba nyaji-ny | grandmother-ERG bird outside-LOC see-PAST",
     "r128_perception_grandmother_bird_outside", 4, js(["babi", "buyi", "ngalkal", "nyajil"]), js([]),
     "Buyi bird object; common-noun perceiver ERG.", "0.84"),
    (28, "We saw the bird outside.",
     "Ngana buyi ngalkal-ba nyajin.",
     "ngana buyi ngalkal-ba nyaji-ny | we bird outside-LOC see-PAST",
     "r128_perception_pronoun_bird_outside", 4, js(["ngana", "buyi", "ngalkal", "nyajil"]), js([]),
     "Pronoun A contrast for bird perception.", "0.84"),
    (29, "The young man saw the snake on the ground.",
     "Warru-ngku jarba bubu-ngu nyajin.",
     "warru-ngku jarba bubu-ngu nyaji-ny | young.man-ERG snake ground-LOC see-PAST",
     "r128_perception_young_man_snake_ground", 4, js(["warru", "jarba", "bubu", "nyajil"]), js([]),
     "Jarba snake perceived object only.", "0.84"),
    (30, "I saw the crocodile outside.",
     "Ngayu bilngkumu ngalkal-ba nyajin.",
     "ngayu bilngkumu ngalkal-ba nyaji-ny | I crocodile outside-LOC see-PAST",
     "r128_perception_pronoun_crocodile_outside", 4, js(["ngayu", "bilngkumu", "ngalkal", "nyajil"]), js([]),
     "Bilngkumu crocodile perceived object only.", "0.84"),

    (31, "The old man asked grandfather about water.",
     "Wulman kami-nda bana-ka babajin.",
     "wulman kami-nda bana-ka babaji-ny | old.man grandfather-LOC:pt water-DAT ask-PAST",
     "r128_ask_old_man_grandfather_water", 4, js(["wulman", "kami", "bana", "babaji"]), js(["wulman"]),
     "Bana-ka retained; wulman loan/contact form logged.", "0.86"),
    (32, "The young man asked grandmother about water.",
     "Warru babi-nda bana-ka babajin.",
     "warru babi-nda bana-ka babaji-ny | young.man grandmother-LOC:pt water-DAT ask-PAST",
     "r128_ask_young_man_grandmother_water", 4, js(["warru", "babi", "bana", "babaji"]), js([]),
     "Ask-about frame retained with sourced water DAT topic.", "0.86"),
    (33, "The woman asked grandfather about news.",
     "Jalbu kami-nda kuku-ku babajin.",
     "jalbu kami-nda kuku-ku babaji-ny | woman grandfather-LOC:pt news-DAT ask-PAST",
     "r128_ask_woman_grandfather_news", 4, js(["jalbu", "kami", "kuku", "babaji"]), js([]),
     "Kuku-ku retained; no ERG on asker.", "0.86"),
    (34, "The friend asked father about meat.",
     "Jawun nganjan-anda minya-ka babajin.",
     "jawun nganjan-anda minya-ka babaji-ny | friend father-LOC:pt meat-DAT ask-PAST",
     "r128_ask_friend_father_meat", 4, js(["jawun", "nganjan", "minya", "babaji"]), js([]),
     "Minya-ka retained in ask-about maintenance row.", "0.86"),
    (35, "The child asked father about the mango.",
     "Karrkay nganjan-anda mayngku-ku babajin.",
     "karrkay nganjan-anda mayngku-ku babaji-ny | child father-LOC:pt mango-DAT ask-PAST",
     "r128_ask_child_father_mango", 4, js(["karrkay", "nganjan", "mayngku", "babaji"]), js(["mayngku"]),
     "Duplicate repair changed only the person asked; mayngku loan is logged.", "0.86"),

    (36, "The old man laughed at father.",
     "Wulman nganjan-anda yajarrin.",
     "wulman nganjan-anda yajarri-ny | old.man father-DAT:pt laugh-PAST",
     "r128_laugh_old_man_father", 3, js(["wulman", "nganjan", "yajarril"]), js(["wulman"]),
     "Duplicate repair changed only the sourced DAT:pt target.", "0.84"),
    (37, "The man laughed at grandmother.",
     "Dingkar babi-nda yajarrin.",
     "dingkar babi-nda yajarri-ny | man grandmother-DAT:pt laugh-PAST",
     "r128_laugh_man_grandmother", 3, js(["dingkar", "babi", "yajarril"]), js([]),
     "DAT:pt target retained; no ERG on laugher.", "0.84"),
    (38, "Father laughed at mother.",
     "Nganjan ngamu-ndu yajarrin.",
     "nganjan ngamu-ndu yajarri-ny | father mother-DAT:pt laugh-PAST",
     "r128_laugh_father_mother", 3, js(["nganjan", "ngamu", "yajarril"]), js([]),
     "DAT:pt target retained.", "0.84"),
    (39, "The girl laughed at grandmother.",
     "Maral babi-nda yajarrin.",
     "maral babi-nda yajarri-ny | girl grandmother-DAT:pt laugh-PAST",
     "r128_laugh_girl_grandmother", 3, js(["maral", "babi", "yajarril"]), js([]),
     "DAT:pt target retained.", "0.84"),
    (40, "The friend laughed at the girl.",
     "Jawun maral-anda yajarrin.",
     "jawun maral-anda yajarri-ny | friend girl-DAT:pt laugh-PAST",
     "r128_laugh_friend_girl", 3, js(["jawun", "maral", "yajarril"]), js([]),
     "DAT:pt target retained.", "0.84"),

    (41, "Grandmother went into the water with the baby.",
     "Babi ngawa-nji bana-nga dungan.",
     "babi ngawa-nji bana-nga dunga-ny | grandmother baby-COMIT:pt water-LOC go-PAST",
     "r128_comit_grandmother_baby_water", 4, js(["babi", "ngawa", "bana", "dungay"]), js([]),
     "Ngawa-nji closed comitative; bana-nga remains LOC.", "0.84"),
    (42, "The young man went into the grass with a friend.",
     "Warru jawun-unji jikan-ba dungan.",
     "warru jawun-unji jikan-ba dunga-ny | young.man friend-COMIT:pt grass-LOC go-PAST",
     "r128_comit_young_man_friend_grass", 4, js(["warru", "jawun", "jikan", "dungay"]), js([]),
     "Jawun-unji closed comitative; jikan-ba remains LOC.", "0.84"),
    (43, "The woman went to the tree with father.",
     "Jalbu nganjan-anji juku-ngu dungan.",
     "jalbu nganjan-anji juku-ngu dunga-ny | woman father-COMIT:pt tree-LOC go-PAST",
     "r128_comit_woman_father_tree", 4, js(["jalbu", "nganjan", "juku", "dungay"]), js([]),
     "Nganjan-anji closed comitative; juku-ngu remains LOC.", "0.84"),
    (44, "The child went to the shade with a stick.",
     "Karrkay juku-ji wabarr-ba dungan.",
     "karrkay juku-ji wabarr-ba dunga-ny | child stick-COMIT shade-LOC go-PAST",
     "r128_comit_child_stick_shade", 4, js(["karrkay", "juku", "wabarr", "dungay"]), js([]),
     "Juku-ji allowed only as sourced closed comitative.", "0.84"),
    (45, "Mother went into the water with clothes.",
     "Ngamu kambi-ji bana-nga dungan.",
     "ngamu kambi-ji bana-nga dunga-ny | mother clothes-COMIT water-LOC go-PAST",
     "r128_comit_mother_clothes_water", 4, js(["ngamu", "kambi", "bana", "dungay"]), js([]),
     "Kambi-ji closed comitative; bana-nga LOC gloss retained.", "0.84"),
    (46, "The old man is staying in the humpy with the baby.",
     "Wulman wurun-ba bundanday ngawa-nji.",
     "wulman wurun-ba bunda-n-bunda-y ngawa-nji | old.man humpy-LOC stay-REDUP-NONPAST baby-COMIT:pt",
     "r128_comit_old_man_baby_humpy", 4, js(["wulman", "wurun", "bunday", "ngawa"]), js(["wulman"]),
     "Wurun-ba humpy LOC; place-before-comitative order retained.", "0.84"),

    (47, "The man saw the snake on the ground.",
     "Dingkar-angka jarba bubu-ngu nyajin.",
     "dingkar-angka jarba bubu-ngu nyaji-ny | man-ERG snake ground-LOC see-PAST",
     "r128_perception_man_snake_ground", 4, js(["dingkar", "jarba", "bubu", "nyajil"]), js([]),
     "Jarba snake perceived object only.", "0.84"),
    (48, "The old woman saw the frog outside.",
     "Wulbuman-angka yirku-yirku ngalkal-ba nyajin.",
     "wulbuman-angka yirku-yirku ngalkal-ba nyaji-ny | old.woman-ERG frog outside-LOC see-PAST",
     "r128_perception_old_woman_frog_outside", 4, js(["wulbuman", "yirku-yirku", "ngalkal", "nyajil"]), js([]),
     "Yirku-yirku frog perceived object only.", "0.84"),
    (49, "I brought paperbark from the house.",
     "Ngayu buyku bayan-muny wundin.",
     "ngayu buyku bayan-muny wundi-ny | I paperbark house-ABL bring-PAST",
     "r128_object_paperbark_house_source", 4, js(["ngayu", "buyku", "bayan", "wundil"]), js([]),
     "Pronoun A bare; buyku object plus ABL source.", "0.84"),
    (50, "We took firewood to camp.",
     "Ngana baya bayan-ka wundin.",
     "ngana baya bayan-ka wundi-ny | we firewood camp-DAT take-PAST",
     "r128_object_firewood_camp_goal", 4, js(["ngana", "baya", "bayan", "wundil"]), js([]),
     "Pronoun A bare; baya object plus DAT goal.", "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("r128_"):
            issues.append(f"{label}: Rule 128 batch frame must start with r128_")
        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}: construction 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")
        if "baya" in loan_values:
            issues.append(f"{label}: baya possible-loan note is unsubstantiated; do not mark it as a loan")
        sense_pairs = [
            ("water", "bana"),
            ("firewood", "baya"),
            ("stick", "juku"),
            ("paperbark", "buyku"),
            ("grass", "jikan"),
            ("humpy", "wurun"),
            ("cloud", "ngurrban"),
            ("wind", "muyar"),
            ("sky", "jiri"),
            ("sun", "wungar"),
            ("bird", "buyi"),
            ("snake", "jarba"),
            ("crocodile", "bilngkumu"),
            ("frog", "yirku-yirku"),
        ]
        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, "baya") and not any(word in analysis_low for word in ("fire", "firewood")):
            issues.append(f"{label}: baya must be glossed fire/firewood")
        if has_token(low, "juku") and not any(word in analysis_low for word in ("tree", "stick", "wood")):
            issues.append(f"{label}: juku must be glossed tree/stick/wood")
        if "INST" in analysis:
            for head in NO_NEW_INSTRUMENT_HEADS:
                if has_token(low, head) and f"{head}-ji" not in low:
                    issues.append(f"{label}: no new instrument from {head}")
        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 128 draft: case-frame consolidation after Rule 127", "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 128 source pass rechecked dictionary.yaml, Patz grammar, existing corpus lexemes, and planned-surface "
            "duplicates before authoring. No new lexical heads or morphology were admitted."
        ),
        command="rg over dictionary.yaml and grammar.md; sqlite lexeme readback and duplicate scan",
        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 retained bana water, baya fire/firewood, juku tree/wood/stick, buyku paperbark, "
            "jikan grass, wurun humpy, ngurrban cloud, muyar wind, jiri sky, wungar sun, buyi bird, jarba snake, "
            "bilngkumu crocodile, and yirku-yirku frog."
        ),
        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 controls retained classifier heads, bana-nga/bana-ka, juku-ji/juku-ngu/juku-muny, jikan-ba, "
            "baya-nga/baya, wungar, ngurrban, muyar, buyku, animal examples, wurun-ba, and janay standing rows."
        ),
        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 new morphology; no new instruments/comitatives/possessives from juku or animal heads; "
            "baya possible-loan note not loan-logged; mayngku and wulman loan/contact forms logged; common-noun "
            "nyaji-l perceivers ERG-marked; babaji-y askers not ERG-marked; closed comitatives 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=(
            "Initial planned scan found two exact duplicates, Karrkay maral-anda mayngku-ku babajin and Wulman kami-nda "
            "yajarrin. They were repaired to Karrkay nganjan-anda mayngku-ku babajin and Wulman nganjan-anda yajarrin. "
            "Final scan found no exact duplicates."
        ),
        command="sqlite exact-surface query over planned Batch 170 rows",
        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 170 stores source inventory, duplicate repairs, static guards, 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 128 construction rows under_review after static guards and duplicate checks.",
        command="python3 batch_scripts/bank_170.py",
        artifact_path="batch_scripts/bank_170.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-10 rest/standing; 11-20 object location/source/goal and humpy perception; 21-30 perception/hearing; "
            "31-35 ask-about; 36-40 laugh targets; 41-46 closed comitatives; 47-50 additional animal/object rows."
        ),
        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())
