#!/usr/bin/env python3
"""Bank batch-165: Rule 123 lexical expansion inside Rule 122 frames."""

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

DENY = {"yah", "ngawuya", "walnga", "maja-nga", "yunga", "bada-bada", "wangkar-wangkar"}
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")


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


ROWS = [
    (1, "The old man stayed at the sea.",
     "Wulman jalun-bu bundandan.",
     "wulman jalun-bu bundanda-ny | old.man sea-LOC stay-PAST",
     "r123_place_old_man_sea", 3, js(["wulman", "jalun", "bunday"]), js(["wulman"]),
     "Grammar narrative examples source wulman as old man/old person; jalun-bu and bundanda-y are Rule 122 controls.", "0.82"),
    (2, "The pig lay in the grass.",
     "Bikibiki jikan-ba wunan.",
     "bikibiki jikan-ba wuna-ny | pig grass-LOC lie-PAST",
     "r123_place_pig_grass", 3, js(["bikibiki", "jikan", "wunay"]), js(["bikibiki"]),
     "Dictionary/Patz source bikibiki as pig; jikan-ba grass-LOC and wuna-y are already controlled.", "0.84"),
    (3, "The dilly bag lay in the shade.",
     "Ngunyin wabarr-ba wunan.",
     "ngunyin wabarr-ba wuna-ny | dilly.bag shade-LOC lie-PAST",
     "r123_place_dillybag_shade", 3, js(["ngunyin", "wabarr", "wunay"]), js([]),
     "Dictionary ngunyin includes dilly bag; wabarr-ba and wuna-y are source-backed.", "0.84"),
    (4, "The fish net lay outside.",
     "Makarr ngalkal-ba wunan.",
     "makarr ngalkal-ba wuna-ny | fish.net outside-LOC lie-PAST",
     "r123_place_net_outside", 3, js(["makarr", "ngalkal", "wunay"]), js([]),
     "Dictionary and Patz ex.162 source makarr fish net; ngalkal-ba outside LOC checked.", "0.84"),
    (5, "The rope lay on the ground.",
     "Karra bubu-ngu wunan.",
     "karra bubu-ngu wuna-ny | rope ground-LOC lie-PAST",
     "r123_place_rope_ground", 3, js(["karra", "bubu", "wunay"]), js([]),
     "Dictionary karra rope/cord/string; bubu-ngu neutral LOC and wuna-y checked.", "0.84"),
    (6, "The lawyer-cane top lay on the ground.",
     "Kaku bubu-ngu wunan.",
     "kaku bubu-ngu wuna-ny | lawyer.cane.top ground-LOC lie-PAST",
     "r123_place_cane_top_ground", 3, js(["kaku", "bubu", "wunay"]), js([]),
     "Dictionary kaku lawyer-cane top; this is the noun sense, not the wait-particle sense.", "0.82"),
    (7, "The bark lay in the grass.",
     "Dumbul jikan-ba wunan.",
     "dumbul jikan-ba wuna-ny | bark grass-LOC lie-PAST",
     "r123_place_bark_grass", 3, js(["dumbul", "jikan", "wunay"]), js([]),
     "Dictionary dumbul bark/hard covering; jikan-ba grass LOC and wuna-y checked.", "0.84"),
    (8, "The mango lay in the bag.",
     "Mayngku balji-nga wunan.",
     "mayngku balji-nga wuna-ny | mango bag-LOC lie-PAST",
     "r123_place_mango_bag", 3, js(["mayngku", "balji", "wunay"]), js(["mayngku"]),
     "Patz/dictionary source mayngku mango; balji-nga bag-LOC is a Rule 122 control.", "0.84"),
    (9, "News came from the house.",
     "Kuku bayan-muny kadan.",
     "kuku bayan-muny kada-ny | news house-ABL come-PAST",
     "r123_source_news_house", 3, js(["kuku", "bayan", "kaday"]), js([]),
     "Dictionary kuku includes news/language; bayan-muny source and kada-y are source-backed.", "0.82"),
    (10, "The old man came from the house.",
     "Wulman bayan-muny kadan.",
     "wulman bayan-muny kada-ny | old.man house-ABL come-PAST",
     "r123_source_old_man_house", 3, js(["wulman", "bayan", "kaday"]), js(["wulman"]),
     "Grammar narrative examples source wulman; bayan-muny ABL source and kada-y checked.", "0.82"),

    (11, "The man put the dilly bag on the road.",
     "Dingkar-angka ngunyin baral-ba yijarrin.",
     "dingkar-angka ngunyin baral-ba yijarri-ny | man-ERG dilly.bag road-LOC put-PAST",
     "r123_object_dillybag_road", 4, js(["dingkar", "ngunyin", "baral", "yijarril"]), js([]),
     "Ngunyin dilly-bag sense checked; baral-ba and yijarri-l keep the Rule 122 object-location frame.", "0.84"),
    (12, "Grandmother left the remains dilly bag outside.",
     "Babi-ngka yinkar ngalkal-ba bawan.",
     "babi-ngka yinkar ngalkal-ba bawa-ny | grandmother-ERG remains.dilly.bag outside-LOC leave-PAST",
     "r123_object_yinkar_outside", 4, js(["babi", "yinkar", "ngalkal", "bawal"]), js([]),
     "Dictionary yinkar is a dilly bag for holding remains; English and gloss expose the narrower sense.", "0.84"),
    (13, "The child put the fish net in the shade.",
     "Karrkay-ngku makarr wabarr-ba yijarrin.",
     "karrkay-ngku makarr wabarr-ba yijarri-ny | child-ERG fish.net shade-LOC put-PAST",
     "r123_object_net_shade", 4, js(["karrkay", "makarr", "wabarr", "yijarril"]), js([]),
     "Makarr fish net is dictionary/Patz-backed; wabarr-ba and yijarri-l are controlled.", "0.84"),
    (14, "The woman put the rope on the ground.",
     "Jalbu-ngku karra bubu-ngu yijarrin.",
     "jalbu-ngku karra bubu-ngu yijarri-ny | woman-ERG rope ground-LOC put-PAST",
     "r123_object_rope_ground", 4, js(["jalbu", "karra", "bubu", "yijarril"]), js([]),
     "Karra rope/cord/string checked; object remains overt before bubu-ngu.", "0.84"),
    (15, "Mother put the lawyer-cane top in the bag.",
     "Ngamu-ngku kaku balji-nga yijarrin.",
     "ngamu-ngku kaku balji-nga yijarri-ny | mother-ERG lawyer.cane.top bag-LOC put-PAST",
     "r123_object_cane_top_bag", 4, js(["ngamu", "kaku", "balji", "yijarril"]), js([]),
     "Kaku noun sense checked; balji-nga bag LOC and transitive A marking retained.", "0.82"),
    (16, "The old woman brought bark from the scrub.",
     "Wulbuman-angka dumbul madja-muny wundin.",
     "wulbuman-angka dumbul madja-muny wundi-ny | old.woman-ERG bark scrub-ABL bring-PAST",
     "r123_object_bark_scrub_source", 4, js(["wulbuman", "dumbul", "madja", "wundil"]), js([]),
     "Dumbul bark checked; madja-muny ABL and wundi-l transfer frame retained.", "0.84"),
    (17, "The man got the fish net at the sea.",
     "Dingkar-angka makarr jalun-bu manin.",
     "dingkar-angka makarr jalun-bu mani-ny | man-ERG fish.net sea-LOC get-PAST",
     "r123_object_net_sea", 4, js(["dingkar", "makarr", "jalun", "manil"]), js([]),
     "Makarr is sourced; jalun-bu place and mani-l get frame retained.", "0.84"),
    (18, "The woman brought the mango from the house.",
     "Jalbu-ngku mayngku bayan-muny wundin.",
     "jalbu-ngku mayngku bayan-muny wundi-ny | woman-ERG mango house-ABL bring-PAST",
     "r123_object_mango_house_source", 4, js(["jalbu", "mayngku", "bayan", "wundil"]), js(["mayngku"]),
     "Mayngku mango checked in Patz/dictionary; bayan-muny source and wundi-l retained.", "0.84"),
    (19, "Grandfather brought news from the house.",
     "Kami-ngka kuku bayan-muny wundin.",
     "kami-ngka kuku bayan-muny wundi-ny | grandfather-ERG news house-ABL bring-PAST",
     "r123_object_news_house_source", 4, js(["kami", "kuku", "bayan", "wundil"]), js([]),
     "Kuku news sense checked; English 'brought news' matches the object-plus-source transfer frame.", "0.82"),
    (20, "We took the mango to camp.",
     "Ngana mayngku bayan-ka wundin.",
     "ngana mayngku bayan-ka wundi-ny | we mango camp-DAT take-PAST",
     "r123_object_mango_camp_goal", 4, js(["ngana", "mayngku", "bayan", "wundil"]), js(["mayngku"]),
     "Pronoun A remains unmarked; mayngku and bayan-ka goal are source-backed.", "0.84"),

    (21, "The child saw the pig in the grass.",
     "Karrkay-ngku bikibiki jikan-ba nyajin.",
     "karrkay-ngku bikibiki jikan-ba nyaji-ny | child-ERG pig grass-LOC see-PAST",
     "r123_perception_child_pig", 4, js(["karrkay", "bikibiki", "jikan", "nyajil"]), js(["bikibiki"]),
     "Bikibiki pig checked; common-noun perceiver remains ERG-marked.", "0.84"),
    (22, "I saw the pig in the grass.",
     "Ngayu bikibiki jikan-ba nyajin.",
     "ngayu bikibiki jikan-ba nyaji-ny | I pig grass-LOC see-PAST",
     "r123_perception_pronoun_pig", 4, js(["ngayu", "bikibiki", "jikan", "nyajil"]), js(["bikibiki"]),
     "Pronoun A remains unmarked; object/location match row 21 for contrast.", "0.84"),
    (23, "The woman saw the dilly bag in the shade.",
     "Jalbu-ngku ngunyin wabarr-ba nyajin.",
     "jalbu-ngku ngunyin wabarr-ba nyaji-ny | woman-ERG dilly.bag shade-LOC see-PAST",
     "r123_perception_woman_dillybag", 4, js(["jalbu", "ngunyin", "wabarr", "nyajil"]), js([]),
     "Ngunyin dilly-bag sense checked; common-noun perceiver marked ERG.", "0.84"),
    (24, "We saw the dilly bag in the shade.",
     "Ngana ngunyin wabarr-ba nyajin.",
     "ngana ngunyin wabarr-ba nyaji-ny | we dilly.bag shade-LOC see-PAST",
     "r123_perception_pronoun_dillybag", 4, js(["ngana", "ngunyin", "wabarr", "nyajil"]), js([]),
     "Pronoun A remains unmarked; ngunyin and wabarr-ba checked.", "0.84"),
    (25, "The man saw the fish net at the sea.",
     "Dingkar-angka makarr jalun-bu nyajin.",
     "dingkar-angka makarr jalun-bu nyaji-ny | man-ERG fish.net sea-LOC see-PAST",
     "r123_perception_man_net", 4, js(["dingkar", "makarr", "jalun", "nyajil"]), js([]),
     "Makarr fish net checked; common-noun perceiver ERG and jalun-bu place retained.", "0.84"),
    (26, "I saw the fish net at the sea.",
     "Ngayu makarr jalun-bu nyajin.",
     "ngayu makarr jalun-bu nyaji-ny | I fish.net sea-LOC see-PAST",
     "r123_perception_pronoun_net", 4, js(["ngayu", "makarr", "jalun", "nyajil"]), js([]),
     "Pronoun A remains unmarked; makarr and jalun-bu checked.", "0.84"),
    (27, "Grandmother heard news from the house.",
     "Babi-ngka kuku bayan-muny nyajin.",
     "babi-ngka kuku bayan-muny nyaji-ny | grandmother-ERG news house-ABL hear-PAST",
     "r123_perception_grandmother_news", 4, js(["babi", "kuku", "bayan", "nyajil"]), js([]),
     "Kuku news/language sense checked; nyaji-l hear sense and common-noun ERG retained.", "0.84"),
    (28, "We heard news from the house.",
     "Ngana kuku bayan-muny nyajin.",
     "ngana kuku bayan-muny nyaji-ny | we news house-ABL hear-PAST",
     "r123_perception_pronoun_news", 4, js(["ngana", "kuku", "bayan", "nyajil"]), js([]),
     "Pronoun A remains unmarked; kuku news plus bayan-muny source checked.", "0.84"),
    (29, "The child saw the sun outside.",
     "Karrkay-ngku wungar ngalkal-ba nyajin.",
     "karrkay-ngku wungar ngalkal-ba nyaji-ny | child-ERG sun outside-LOC see-PAST",
     "r123_perception_child_sun", 4, js(["karrkay", "wungar", "ngalkal", "nyajil"]), js([]),
     "Wungar sun checked; common-noun perceiver ERG and ngalkal-ba location retained.", "0.84"),
    (30, "I saw the mango in the bag.",
     "Ngayu mayngku balji-nga nyajin.",
     "ngayu mayngku balji-nga nyaji-ny | I mango bag-LOC see-PAST",
     "r123_perception_pronoun_mango", 4, js(["ngayu", "mayngku", "balji", "nyajil"]), js(["mayngku"]),
     "Pronoun A remains unmarked; mayngku and balji-nga checked.", "0.84"),

    (31, "The girl asked grandmother about news.",
     "Maral babi-nda kuku-ku babajin.",
     "maral babi-nda kuku-ku babaji-ny | girl grandmother-LOC:pt news-DAT ask-PAST",
     "r123_ask_grandmother_news", 4, js(["maral", "babi", "kuku", "babaji"]), js([]),
     "Patz ex.584 sources kuku-ku language/news-DAT; babaji-y asker remains ABS/S.", "0.86"),
    (32, "The young man asked grandfather about the mango.",
     "Warru kami-nda mayngku-ku babajin.",
     "warru kami-nda mayngku-ku babaji-ny | young.man grandfather-LOC:pt mango-DAT ask-PAST",
     "r123_ask_grandfather_mango", 4, js(["warru", "kami", "mayngku", "babaji"]), js(["mayngku"]),
     "Patz ex.602 sources mayngku-ku mango-DAT; no ERG on the asker.", "0.86"),
    (33, "The woman asked father about food.",
     "Jalbu nganjan-anda mayi-ka babajin.",
     "jalbu nganjan-anda mayi-ka babaji-ny | woman father-LOC:pt food-DAT ask-PAST",
     "r123_ask_father_food", 4, js(["jalbu", "nganjan", "mayi", "babaji"]), js([]),
     "Babaji-y frame retained with ABS/S asker, LOC:pt person, and mayi-ka DAT topic.", "0.86"),
    (34, "The friend asked mother about news.",
     "Jawun ngamu-ndu kuku-ku babajin.",
     "jawun ngamu-ndu kuku-ku babaji-ny | friend mother-LOC:pt news-DAT ask-PAST",
     "r123_ask_mother_news", 4, js(["jawun", "ngamu", "kuku", "babaji"]), js([]),
     "Kuku-ku is grammar-sourced; jawun remains ABS/S in the fixed intransitive ask-about frame.", "0.86"),
    (35, "The child asked the girl about the mango.",
     "Karrkay maral-anda mayngku-ku babajin.",
     "karrkay maral-anda mayngku-ku babaji-ny | child girl-LOC:pt mango-DAT ask-PAST",
     "r123_ask_girl_mango", 4, js(["karrkay", "maral", "mayngku", "babaji"]), js(["mayngku"]),
     "Mayngku-ku is grammar-sourced; maral-anda person asked and ABS/S asker retained.", "0.86"),

    (36, "The old man laughed at the child.",
     "Wulman karrkay-anka yajarrin.",
     "wulman karrkay-anka yajarri-ny | old.man child-DAT:pt laugh-PAST",
     "r123_laugh_old_man_child", 3, js(["wulman", "karrkay", "yajarril"]), js(["wulman"]),
     "Wulman participant noun checked; yajarri-l target remains karrkay-anka DAT:pt.", "0.84"),
    (37, "Mother laughed at grandfather.",
     "Ngamu kami-nda yajarrin.",
     "ngamu kami-nda yajarri-ny | mother grandfather-DAT:pt laugh-PAST",
     "r123_laugh_mother_grandfather", 3, js(["ngamu", "kami", "yajarril"]), js([]),
     "Laugher is intransitive S; kami-nda DAT:pt target retained.", "0.84"),
    (38, "Father laughed at the young man.",
     "Nganjan warru-ndu yajarrin.",
     "nganjan warru-ndu yajarri-ny | father young.man-DAT:pt laugh-PAST",
     "r123_laugh_father_young_man", 3, js(["nganjan", "warru", "yajarril"]), js([]),
     "Warru-ndu target form retained from Rule 122 source controls.", "0.84"),
    (39, "The girl laughed at the woman.",
     "Maral jalbu-ndu yajarrin.",
     "maral jalbu-ndu yajarri-ny | girl woman-DAT:pt laugh-PAST",
     "r123_laugh_girl_woman", 3, js(["maral", "jalbu", "yajarril"]), js([]),
     "Jalbu-ndu DAT:pt target retained; no ERG on the laugher.", "0.84"),
    (40, "The friend laughed at father.",
     "Jawun nganjan-anka yajarrin.",
     "jawun nganjan-anka yajarri-ny | friend father-DAT:pt laugh-PAST",
     "r123_laugh_friend_father", 3, js(["jawun", "nganjan", "yajarril"]), js([]),
     "Nganjan-anka target form retained; subject remains intransitive S.", "0.84"),

    (41, "The old man is staying in the house with the baby.",
     "Wulman bayan-ba bundanday ngawa-nji.",
     "wulman bayan-ba bunda-n-bunda-y ngawa-nji | old.man house-LOC stay-REDUP-NONPAST baby-COMIT:pt",
     "r123_comit_old_man_baby", 4, js(["wulman", "bayan", "bunday", "ngawa"]), js(["wulman"]),
     "Ngawa-nji remains direct-source only from Patz ex.549; wulman subject changes the row without new morphology.", "0.84"),
    (42, "Grandmother went to the road with a friend.",
     "Babi jawun-unji baral-ba dungan.",
     "babi jawun-unji baral-ba dunga-ny | grandmother friend-COMIT:pt road-LOC go-PAST",
     "r123_comit_grandmother_friend_road", 4, js(["babi", "jawun", "baral", "dungay"]), js([]),
     "Jawun-unji is in the closed comitative set; baral-ba road goal/source control retained.", "0.84"),
    (43, "The young man went to the sea with father.",
     "Warru nganjan-anji jalun-bu dungan.",
     "warru nganjan-anji jalun-bu dunga-ny | young.man father-COMIT:pt sea-LOC go-PAST",
     "r123_comit_young_man_father_sea", 4, js(["warru", "nganjan", "jalun", "dungay"]), js([]),
     "Nganjan-anji is in the closed comitative set; jalun-bu sea LOC retained.", "0.84"),
    (44, "The woman went to the road with a stick.",
     "Jalbu juku-ji baral-ba dungan.",
     "jalbu juku-ji baral-ba dunga-ny | woman stick-COMIT road-LOC go-PAST",
     "r123_comit_woman_stick_road", 4, js(["jalbu", "juku", "baral", "dungay"]), js([]),
     "Juku-ji is a sourced neutral comitative; no new object comitative is introduced.", "0.84"),
    (45, "Mother went to the shade with clothes.",
     "Ngamu kambi-ji wabarr-ba dungan.",
     "ngamu kambi-ji wabarr-ba dunga-ny | mother clothes-COMIT shade-LOC go-PAST",
     "r123_comit_mother_clothes_shade", 4, js(["ngamu", "kambi", "wabarr", "dungay"]), js([]),
     "Kambi-ji is in the closed comitative set; kambi has clothing sense here.", "0.84"),
    (46, "Grandmother came from the house with the baby.",
     "Babi ngawa-nji bayan-muny kadan.",
     "babi ngawa-nji bayan-muny kada-ny | grandmother baby-COMIT:pt house-ABL come-PAST",
     "r123_comit_grandmother_baby_house_source", 4, js(["babi", "ngawa", "bayan", "kaday"]), js([]),
     "Ngawa-nji remains direct-source only; bayan-muny source and kada-y checked.", "0.84"),

    (47, "The man left the dog's food outside.",
     "Dingkar-angka mayi kaya-nga ngalkal-ba bawan.",
     "dingkar-angka mayi kaya-nga ngalkal-ba bawa-ny | man-ERG food dog-POSS.ABS outside-LOC leave-PAST",
     "r123_poss_food_man_leave", 5, js(["dingkar", "mayi", "kaya", "ngalkal", "bawal"]), js([]),
     "Patz-backed mayi kaya-nga reused only as a fixed possessive object; no new possessor template.", "0.84"),
    (48, "The woman put the dog's food in the bag.",
     "Jalbu-ngku mayi kaya-nga balji-nga yijarrin.",
     "jalbu-ngku mayi kaya-nga balji-nga yijarri-ny | woman-ERG food dog-POSS.ABS bag-LOC put-PAST",
     "r123_poss_food_woman_put", 5, js(["jalbu", "mayi", "kaya", "balji", "yijarril"]), js([]),
     "Mayi kaya-nga and balji-nga are Patz/Rule 122 controls; A marked ERG.", "0.84"),
    (49, "The young man took mother's clothes from the house.",
     "Warru-ngku kambi ngamu-ngu bayan-muny wundin.",
     "warru-ngku kambi ngamu-ngu bayan-muny wundi-ny | young.man-ERG clothes mother-POSS.ABS house-ABL take-PAST",
     "r123_poss_clothes_young_man_source", 5, js(["warru", "kambi", "ngamu", "bayan", "wundil"]), js([]),
     "Kambi ngamu-ngu is reused only as Patz-backed possessive object; bayan-muny source retained.", "0.84"),
    (50, "I took mother's clothes to camp.",
     "Ngayu kambi ngamu-ngu bayan-ka wundin.",
     "ngayu kambi ngamu-ngu bayan-ka wundi-ny | I clothes mother-POSS.ABS camp-DAT take-PAST",
     "r123_poss_clothes_pronoun_goal", 5, js(["ngayu", "kambi", "ngamu", "bayan", "wundil"]), js([]),
     "Pronoun A unmarked; kambi ngamu-ngu fixed possessive object and bayan-ka goal retained.", "0.84"),
]


def has_denied_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()
        analysis_low = analysis.lower()
        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("r123_"):
            issues.append(f"{label}: Rule 123 batch frame must start with r123_")
        if low in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface within batch")
        seen_ku.add(low)
        for marker in CONSTRUCTION_CLITIC_MARKERS:
            if marker in low:
                issues.append(f"{label}: lexical batch must not reuse temporal clitic marker {marker}")
        for denied in DENY:
            if has_denied_token(low, denied):
                issues.append(f"{label}: denied or out-of-scope token {denied!r}")
        if "baral-muny" in low:
            issues.append(f"{label}: baral-muny is not source-logged")
        if "kadar-anji" in low:
            issues.append(f"{label}: kadar-anji is outside this batch's closed comitative set")
        if "ngunyin-ji" in low:
            issues.append(f"{label}: ngunyin-ji is attested but not admitted by Rule 123's closed-comitative policy")
        if "kija" in low and not re.search(r"\b(moon|month)\b", analysis_low):
            issues.append(f"{label}: kija is moon/month only; analysis must say moon/month")
        if "star" in english.lower() and "dawar" not in low:
            issues.append(f"{label}: star row must use dawar")
        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 any(form in low for form in ("kaya-nga", "ngamu-ngu")) and "POSS" not in analysis:
            issues.append(f"{label}: possessive form needs explicit POSS analysis")
        try:
            json.loads(words_used)
            json.loads(loans_used)
        except json.JSONDecodeError as exc:
            issues.append(f"{label}: JSON field failed: {exc}")
        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 123 draft: source-backed lexical expansion inside Rule 122 frames", "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 123 source pass checked dictionary.yaml, Patz grammar, and existing corpus lexemes before authoring. "
            "The batch adds source-backed lexical heads inside existing Rule 122 case frames without new morphology."
        ),
        command="rg/sed over dictionary.yaml and grammar.md; sqlite readback from lexemes",
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="source",
        event_type="dictionary-source-check",
        actor="claude-main",
        detail=(
            "Exact dictionary readback confirmed ngunyin, yinkar, makarr, karra, kaku, dumbul, bikibiki, mayngku, "
            "kuku, wungar, plus reused heads and verbs. Sense constraints: ngunyin=dilly bag, yinkar=special dilly bag, "
            "kaku=lawyer-cane top, dumbul=bark/hard covering, kuku=news/language, bikibiki=pig, mayngku=mango."
        ),
        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 used: kuku-ku language/news-DAT, mayngku-ku mango-DAT, makarr with wundi-l, bikibiki "
            "examples, wulman narrative use, and the retained Rule 122 inventory for place/source, ask-about, laugh "
            "targets, perception A marking, closed comitatives, and possessive object NPs."
        ),
        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 ngunyin-ji despite attestation; no new comitatives; no free possessive templates; no "
            "baral-muny; no kadar-anji; no maja-nga or yunga-l; no ergative babaji-y askers; common-noun nyaji-l "
            "perceivers ERG-marked; pronoun A unmarked; yajarri-l targets DAT:pt."
        ),
        artifact_path=PROCESS_LOG,
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="preflight",
        event_type="existing-surface-scan",
        actor="claude-main",
        detail="Checked all 50 planned Kuku surfaces against the full non-rejected corpus; no exact duplicates found before insert.",
        command="sqlite exact-surface query over planned Batch 165 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 165 stores source inventory, sense restrictions, guard design, row grouping, duplicate scan, static "
            "checks, bank outcome, and later lint/review/close/export/docs evidence as process_events plus the 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 123 lexical-expansion rows under_review after static guards and duplicate checks.",
        command="python3 batch_scripts/bank_165.py",
        artifact_path="batch_scripts/bank_165.py",
        created_at=NOW,
    )
    record_event(
        db,
        batch_label=LABEL,
        phase="bank",
        event_type="row-inventory",
        actor="claude-main",
        detail=(
            "Rows 1-10 place/source/rest with new heads; 11-20 object place/source/goal; 21-30 perception contrasts; "
            "31-35 ask-about with kuku-ku/mayngku-ku; 36-40 laugh targets; 41-46 closed comitatives; 47-50 fixed possessive objects."
        ),
        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())
