"""q04_check.py: the Phase 3 check script. A phase stop may not be requested unless it prints PASS.

Groups of assertions
  A  values files: schema, kinds, sources, finite numbers, no name shared by the two files, judgments labelled
  B  venue checklist: every quote verified on the page in one of the two readings, every v_* number found in its
     quote, every rule citing existing quotes and values, every guideline quote used, every heading of the
     guidelines page accounted for, every conflict routed to a question
  C  independent recomputation of every population value that a testability or applicability rule reads, the
     Phase 2 indicator reproduced by the pre-registered code, and code constants equal to the registered settings
  D  plan schema and rule grammar: expressions parse without literals, names exist or are planned Phase 4 names,
     rules equal magnitude and precision, thresholds beyond their nulls, no confirmatory statistic in any values
     file, no Phase 3 script calling a confirmatory function on data, variants consistent
  E  satisfiability (pipeline change C20) on the measured population
  F  machine-checkability dry run of the classifier and the tally on synthetic values
  S  the frozen estimators of q_confirm.py run on synthetic data with planted effects (q05_synthetic.py)
  G  budget and structure against the venue limits
  H  rendering of the plan and of the review record: tokens resolve, no hand-typed digit
  I  language lint of both documents (Phase 2 lint, Phase 3 allow-list): zero errors
  J  freeze: the recorded sha256 of plan.json, q_common.py, q_confirm.py and q_rules.py equal the files on disk
  K  the plan and the review record on disk carry this check's result and equal a fresh render
"""
import ast, datetime, glob, hashlib, inspect, json, math, os, re, sys

import numpy as np
import pandas as pd

sys.path.insert(0, "/home/claude/mill/code")
sys.path.insert(0, "/home/claude/mill/phase3/code")
from vals import TOKEN
import p07_lint
import q_common as QC
import q_confirm as QF
import q_rules as QR
import q_render as RD
import q05_synthetic as SY

P3 = "/home/claude/mill/phase3"
N = 0
FAIL = []


def check(cond, what):
    global N
    N += 1
    if not bool(cond):
        FAIL.append(what)
    return bool(cond)


V2 = json.load(open("/home/claude/mill/outputs/values.json"))
V3 = json.load(open(f"{P3}/outputs/values3.json"))
D = RD.load_inputs()
PLAN, RULES, QT, RDOC = D["plan"], D["rules"], D["quotes"], D["rules_doc"]
DER = RD.derived(D)
ALL = {**V2, **V3, **DER}
val = lambda k: ALL[k]["value"]

# ============================================================== A. values files
check(not (set(V2) & set(V3)), "A no name in both values files")
check(not (set(DER) & (set(V2) | set(V3))), "A derived render values do not shadow stored values")
KINDS = {"value", "judgment", "external", "text", "interval"}
scripts = {os.path.basename(p) for p in glob.glob(f"{P3}/code/*.py")}
for k, r in V3.items():
    check(re.fullmatch(r"[a-z0-9_]+", k), f"A name {k}")
    check(all(f in r for f in ("value", "unit", "estimand", "source", "kind")), f"A fields {k}")
    check(r.get("kind") in KINDS, f"A kind {k}")
    check(r.get("source") in scripts, f"A source script exists for {k}")
    check(isinstance(r.get("estimand"), dict) and r["estimand"].get("statistic"), f"A estimand present {k}")
    v = r.get("value")
    if isinstance(v, float):
        check(math.isfinite(v), f"A finite {k}")
    if r.get("kind") == "judgment":
        check(r["estimand"].get("population") == "analyst judgment", f"A judgment labelled {k}")
    if r.get("kind") in ("value", "judgment", "external") and r.get("unit") not in ("text", "date"):
        check(isinstance(v, (int, float)) and not isinstance(v, bool), f"A numeric value {k}")
check(all(k in V3 for k in ("alpha_family", "thr_auc_drop", "thr_kappa_frac", "speed_class_edge_rpm", "min_pos_testable", "max_nonfinite_share")), "A every decision setting registered")
check(0 < val("alpha_family") < 1 and 0 < val("max_nonfinite_share") < 1, "A levels are probabilities")

# ============================================================== B. venue checklist
Q = QT["quotes"]
Q1c, Q2c = QT["quote_check"], QT["quote_check_round2"]
check(Q1c["failed"] == [] and Q2c["failed"] == [], "B no quote failed a verbatim check")
check(Q1c["n_checked"] + Q2c["n_checked"] == len(Q), "B every stored quote was checked in one of the two readings")
check(set(Q2c["ids"]) <= set(Q) and len(Q2c["ids"]) == Q2c["n_checked"], "B the second reading lists the quotes it checked")
check(Q1c["page_text_sha256_at_check"] == QT["source"]["page_text_sha256"] == Q2c["page_text_sha256_at_check"], "B page text identical at both readings")
check(val("v_quotes_checked") == len(Q) and val("v_quotes_failed") == 0 and val("v_quotes_checked_round2") == Q2c["n_checked"], "B values file records the quote checks")
check(val("v_source_url") == QT["source"]["url"] and val("v_retrieved_date") == QT["source"]["retrieved_utc"][:10], "B source URL and date from the retrieval record")
check(val("v_u1_url") == QT["other_sources"]["S_ijamt_u1"]["url"] and val("v_sn_latex_url") == QT["other_sources"]["S_sn_latex"]["url"], "B other source URLs from the retrieval record")
WORDS = {2: "two", 3: "three", 4: "four", 5: "five", 6: "six"}
for k, r in V3.items():
    if k.startswith("v_") and r["kind"] == "external":
        m = re.search(r"quote (q_\w+)", r["estimand"]["filter"])
        if not m:
            continue
        q, v = Q[m.group(1)], r["value"]
        cands = {str(v), f"{v:g}"}
        if isinstance(v, (int, float)):
            if float(v).is_integer():
                cands |= {str(int(v)), WORDS.get(int(v), "@")}
            cands.add(f"{v * 100:g} %")
        check(any(c in q for c in cands), f"B {k} = {v} appears in {m.group(1)}")
check(val("v_template_version") in QT["other_sources"]["S_sn_latex"]["quotes"]["q_pkg"], "B template version in its quote")
check(f"{val('v_template_zip_kb'):.2f}" in QT["other_sources"]["S_sn_latex"]["quotes"]["q_zip"], "B template size in its quote")
check(not any(c in q for q in Q.values() for c in "\u2013\u2014\u2015\u2012"), "B no dash character in any stored quote")
ids = [x["id"] for x in RULES]
check(len(ids) == len(set(ids)) and ids == [f"R{i:02d}" for i in range(1, len(ids) + 1)], "B rule ids unique and consecutive")
qids = {z["id"] for z in PLAN["questions"]}
cited = set()
for x in RULES:
    for q in x["quotes"]:
        try:
            RD.quote_text(QT, q); ok = True
        except KeyError:
            ok = False
        check(ok, f"B {x['id']} cites an existing quote {q}")
        cited.add(q)
    names = {m.group(1) for m in TOKEN.finditer(x["constraint"])}
    check(names <= set(x["values"]), f"B {x['id']} lists every value its constraint cites")
    check(all(n in ALL for n in x["values"]), f"B {x['id']} values exist")
    check(x["phase"] in (4, 5) and isinstance(x["machine"], bool), f"B {x['id']} phase and machine flag")
    check(x["quotes"] or x.get("gap"), f"B {x['id']} rests on a quote or states its gap")
    if x.get("conflict"):
        qn = re.findall(r"\bQ[1-9]\b", x["conflict"])
        check(bool(qn) and all(q in qids for q in qn), f"B {x['id']} conflict routed to an existing question")
check(set(Q) <= cited, f"B every guideline quote is used by a rule (unused: {sorted(set(Q) - cited)})")
heads = QT["page_headings"]["headings"]
content = {h["i"] for h in heads if h["chars"] > 0 and h["heading"] != "Contents"}
covered = [i for e in RDOC["sections"] for i in e["page"]]
check(sorted(covered) == sorted(content), f"B every heading of the guidelines page with text is accounted for exactly once ({sorted(content ^ set(covered))})")
check(val("v_page_headings") == len(content), "B values file records the number of headings")
for e in RDOC["sections"]:
    check(all(heads[i]["heading"] == h for i, h in zip(e["page"], e["headings"])), f"B accounting entry names match the page headings {e['headings'][:1]}")
    check(all(r in ids for r in e["rules"]) and (e["rules"] or e.get("note")), f"B accounting entry maps to rules or states why not {e['headings'][:1]}")
check(set(ids) <= {r for e in RDOC["sections"] for r in e["rules"]} | {"R27"}, "B every rule traces to a section of the page (R27 records a gap)")
check("Q3" in qids and "Q3" in next(x for x in RULES if x["id"] == "R10")["enforce"], "B template download routed to Q3")

# ============================================================== C. independent recomputation of every rule input
T = pd.read_parquet("/home/claude/mill/outputs/segments.parquet")
T["family"] = T.Tool.str.replace("*", "", regex=False)
inv = pd.read_csv("/home/claude/mill/outputs/inventory_runs.csv")
early = {(m, r) for m, r, s in zip(inv.machine, inv.run, inv.summary_remark.fillna("")) if "early stop" in s.lower()}
base = T[(T.machine == "imi_vm20i") & T.label.isin([0, 1])].copy()
base["early"] = [("imi_vm20i", r) in early for r in base.run]
VAR = {"primary": base, "no_twins": base[~base.Tool.str.endswith("*")], "no_early": base[~base.early]}


def folds_min(d, repeats, k, seed):
    """Fewest label 1 paths in a run-held-out training fold, fold assignment rebuilt here (same algorithm)."""
    rng = np.random.default_rng(seed); worst = 10 ** 9
    for _ in range(repeats):
        runs = np.array(sorted(set(d.run_id))); rng.shuffle(runs)
        f = {r: i % k for i, r in enumerate(runs)}
        fold = np.array([f[r] for r in d.run_id])
        for j in range(k):
            worst = min(worst, int(d.label.values[fold != j].sum()))
    return worst


def kish(counts):
    c = np.asarray(counts, float)
    return float(c.sum() ** 2 / (c ** 2).sum())


recomputed = {}
for vn, d in VAR.items():
    for fset in ("highrate", "mtc"):
        dd = d.dropna(subset=QC.FEATS[fset])
        fams = sorted(dd.family.unique())
        recomputed[f"pop_h_{fset}_{vn}_min_train_l1_family"] = min(int(dd[dd.family != f].label.sum()) for f in fams)
        recomputed[f"pop_h_{fset}_{vn}_min_train_l0_family"] = min(int((dd[dd.family != f].label == 0).sum()) for f in fams)
        recomputed[f"pop_h_{fset}_{vn}_min_train_l1_run"] = folds_min(dd, int(val("s1_repeats")), int(val("s1_folds")), QC.SEED)
    l1v = d[d.label == 1]
    recomputed[f"pop_kish_l1_runs_{vn}"] = kish(l1v.groupby("run").size())
    h3 = l1v[l1v.acc0_nh_peak_ratio.notna() & l1v.s2_nh_peak_ratio.notna()]
    for cls, m in (("lo", h3.N < val("speed_class_edge_rpm")), ("hi", h3.N >= val("speed_class_edge_rpm"))):
        recomputed[f"pop_h3_{vn}_{cls}_n"] = int(m.sum())
        recomputed[f"pop_h3_{vn}_{cls}_runs"] = int(h3[m].run.nunique())
        recomputed[f"pop_h3_{vn}_{cls}_families"] = int(h3[m].family.nunique())
rule_inputs = set()
for p in PLAN["predictions"]:
    rule_inputs |= {n for n in QR.names(p["testable"]) if n.startswith("pop_")}
    for v in p["variants"]:
        rule_inputs |= {n for n in QR.names(v["applicable"]) if n.startswith("pop_")}
for n in sorted(rule_inputs):
    check(n in recomputed, f"C rule input {n} has an independent recomputation")
    if n in recomputed and n in V3:
        a, b = recomputed[n], val(n)
        check(abs(a - b) < 1e-9, f"C rule input {n}: registered {b}, recomputed {a}")
check(val("pop_h_highrate_primary_n") == len(base.dropna(subset=QC.FEATS["highrate"])) == len(base), "C every labelled imi_vm20i path has the high-rate features")
check(val("pop_h_mtc_primary_n") == len(base.dropna(subset=QC.FEATS["mtc"])) == len(base), "C every labelled imi_vm20i path has the MTConnect features")
check(val("pop_h_highrate_primary_n_l1") == int(base.label.sum()) == val("d3_n1_imi_vm20i_acc0"), "C label 1 count")
for fam, g in base.groupby("family"):
    check(val(f"pop_imi_vm20i_{fam.lower()}_l1") == int(g.label.sum()) and val(f"pop_imi_vm20i_{fam.lower()}_l0") == int((g.label == 0).sum()), f"C family {fam} counts")
sh = [base[base.family != f].label.mean() for f in sorted(base.family.unique())]
check(abs(val("pop_h_highrate_primary_train_l1_share_min_family") - min(sh)) < 1e-12 and abs(val("pop_h_highrate_primary_train_l1_share_max_family") - max(sh)) < 1e-12, "C training label 1 share range")
l1 = base[base.label == 1]
fam_run = l1.groupby("run").family.first(); per_run = l1.groupby("run").size()
check(abs(val("pop_kish_l1_runs_other") - kish(per_run[fam_run != "A"])) < 1e-12 and abs(val("pop_kish_l1_runs_a") - kish(per_run[fam_run == "A"])) < 1e-12 and abs(val("pop_kish_l1_runs_e") - kish(per_run[fam_run == "E"])) < 1e-12, "C effective runs by family group")
check(val("pop_l1_runs_other_single") == int((per_run[fam_run != "A"] == 1).sum()), "C single-path runs outside family A")
top3 = per_run[fam_run != "A"].sort_values(ascending=False).iloc[:3].sum() / per_run[fam_run != "A"].sum()
check(abs(val("pop_l1_other_top3_share") - top3) < 1e-12, "C share of the three largest runs outside family A")
check(val("pop_kish_l1_runs_other") < val("min_runs_interpretable"), "C the family comparison is below the effective-run floor (why it left the confirmatory set)")
check(val("pop_bytool_primary_other_runs") == per_run[fam_run != "A"].size and val("pop_bytool_primary_other_n") == int(per_run[fam_run != "A"].sum()), "C label 1 runs and paths outside family A")
check(val("pop_agree_excluded_slow") == int((base.N < val("d5_min_rpm")).sum()) and val("pop_agree_excluded_slow_l1") == int(((base.N < val("d5_min_rpm")) & (base.label == 1)).sum()), "C paths below the comb floor")
pa, pb = val("d5_nhdom_imi_vm20i_acc0_l1"), val("d5_nhdom_imi_vm20i_s2_l1")
pe = pa * pb + (1 - pa) * (1 - pb)
check(abs(val("kappa_max_l1_pooled") - (1 - abs(pa - pb) - pe) / (1 - pe)) < 1e-12, "C pooled attainable kappa in label 1 from the D5 shares")
rel = (base.mtc_Spindle_Speed - base.N) / base.N
check(abs(val("pop_mtc_speed_equals_n_share") - float((rel.abs() < 1e-9).mean())) < 1e-12, "C MTConnect speed equals N")
for ch, dfh in (("acc0", 51200.0 / 8192), ("s2", 48000.0 / 8192)):
    for nm, rpm in (("d5_min", val("d5_min_rpm")), ("speed_class_edge", val("speed_class_edge_rpm"))):
        check(abs(val(f"comb_offfrac_{ch}_at_{nm}") - max(0.0, 1 - (2 * val("d5_comb_bins") + 1) * dfh / (rpm / 60))) < 1e-12, f"C comb geometry {ch} at {nm}")
# the pre-registered indicator code reproduces the Phase 2 indicator, channel by channel (no cross-tabulation)
for ch in ("acc0", "acc1", "s1", "s2"):
    stored = np.where(base[f"{ch}_nh_peak_ratio"].notna(), (base[f"{ch}_nh_peak_ratio"] > base[f"{ch}_h_peak_ratio"]).astype(float), np.nan)
    new = QC.nh_indicator(base, ch)
    same_nan = np.array_equal(np.isnan(stored), np.isnan(new))
    check(same_nan and np.array_equal(stored[~np.isnan(stored)], new[~np.isnan(new)]), f"C q_common.nh_indicator reproduces the Phase 2 {ch} indicator on every path")
    for lab in (0, 1):
        k = f"d5_nhdom_imi_vm20i_{ch}_l{lab}"
        if k in V2:
            m = (base.label.values == lab) & ~np.isnan(new)
            check(abs(np.nanmean(new[m]) - val(k)) < 1e-12, f"C {k} reproduced")
# code constants equal the registered settings
check(QC.SEED == val("seed_analysis"), "C seed in code equals seed_analysis")
rf = QC.model("rf")
check(rf.n_estimators == val("rf_trees") and rf.min_samples_leaf == val("rf_min_leaf") and rf.class_weight == "balanced", "C random forest settings equal the registered ones")
lr = QC.model("lr", val("lr_c"))
check(lr.steps[-1][1].C == val("lr_c") and lr.steps[-1][1].class_weight == "balanced", "C logistic settings: lr_c and balanced weights")
sig = inspect.signature(QC.oof_run_matrix).parameters
check(sig["repeats"].default == val("s1_repeats") and sig["k"].default == val("s1_folds") and sig["balanced"].default is True, "C run-held-out defaults equal the Phase 2 screen settings, balanced by default")
sa = inspect.signature(QF.auc_drop).parameters
check(sa["repeats"].default == val("s1_repeats") and sa["k"].default == val("s1_folds") and sa["level"].default == val("ci_level"), "C auc_drop defaults equal the registered settings")
sc_ = inspect.signature(QF.cooccurrence).parameters
check(sc_["frac"].default == val("thr_kappa_frac") and sc_["level"].default == val("ci_level"), "C cooccurrence defaults equal the registered settings")
check(inspect.signature(QC.oof_family).parameters["balanced"].default is True, "C family-held-out balanced by default")
check(inspect.signature(QC.winsorize_fit).parameters["q"].default == val("winsor_q"), "C winsorising share equals winsor_q")
sn = inspect.signature(QC.nh_indicator).parameters
check(sn["comb_half_bins"].default == val("d5_comb_bins") and tuple(sn["band"].default) == (val("d5_band_lo_hz"), val("d5_band_hi_hz")) and sn["min_spindle_hz"].default * 60 == val("d5_min_rpm") and sn["rel_speed_tol"].default == 0.0, "C indicator defaults equal the D5 settings")
check(QF.AUC_NULL == 0.0 and QF.KAPPA_NULL == 0.0, "C the nulls of the frozen estimators are zero")
VS = QF.VARIANTS
check(VS["v6a"]["comb_half_bins"] == val("comb_bins_v6a") and VS["v6b"]["comb_half_bins"] == val("comb_bins_v6b"), "C comb variants in code equal the registered widths")
check(VS["v6c"]["rel_speed_tol"] == val("rel_speed_tol") and VS["v7"]["band"] == (val("d5_band_lo_hz"), val("band_hi_v7_hz")), "C relative comb and band variants in code equal the registered settings")
check(VS["v3"] == {"kind": "rf"} and VS["v5"] == {"winsor": True} and VS["v1"] == {"population": "no_twins"} and VS["v2"] == {"population": "no_early"} and VS["v9"] == {"strata": "family_speed"}, "C the remaining variants change a single setting each")
check(all(len(v) == 1 for v in VS.values()), "C every variant in code changes exactly one setting")
_syn = pd.DataFrame({"Tool": ["A", "A*", "E"], "run": [1, 2, 3], "N": [3000.0, 12000.0, 9000.0], "family": ["A", "A", "E"]})
check(len(QF.population(_syn, "no_twins", set())) == 2 and len(QF.population(_syn, "no_early", {2})) == 2 and len(QF.population(_syn, "primary", {2})) == 3, "C variant populations drop exactly the twins or the early-stopped runs")
check(list(QF.h3_strata(_syn, val("speed_class_edge_rpm"))) == [0, 1, 1] and len(set(QF.h3_strata(_syn, val("speed_class_edge_rpm"), "family_speed"))) == 3, "C H3 strata follow the registered edge, crossed with family for V9")

# ============================================================== D. plan schema, rule grammar, no peeking
for key in ("paper", "rq", "negatives", "format", "budget", "quantities", "predictions", "variants", "exploratory", "methods",
            "assumptions", "missing", "outliers", "multiplicity", "preregistration", "taxonomy", "structure", "failure_modes",
            "confidence", "decisions", "questions", "declared_not_applicable", "variant_naming"):
    check(key in PLAN, f"D plan has {key}")
preds = PLAN["predictions"]
check([p["id"] for p in preds] == ["H1", "H2", "H3"], "D predictions H1 to H3 in order")
QN = PLAN["quantities"]
check(all(re.fullmatch(r"h[1-3]_[a-z0-9_]+", k) for k in QN), "D planned names carry a prediction prefix")
peek = [k for k in list(V2) + list(V3) if re.match(r"h[1-4]_|tally_", k)]
check(not peek, f"D no confirmatory statistic or tally exists in any values file ({peek[:5]})")
FORBID = {"oof_family", "auc_drop", "cooccurrence", "kappa_w", "kappa_null_w", "kappa_max_w", "share_diff_w", "within_family_auc", "boot_weights", "holm_adjust"}
for f in sorted(x for x in glob.glob(f"{P3}/code/q0*_*.py") if not x.endswith("q05_synthetic.py")) + [f"{P3}/code/q_render.py"]:
    tree = ast.parse(open(f).read())
    calls = {(n.func.attr if isinstance(n.func, ast.Attribute) else getattr(n.func, "id", "")) for n in ast.walk(tree) if isinstance(n, ast.Call)}
    check(not (calls & FORBID), f"D {os.path.basename(f)} calls no confirmatory function ({sorted(calls & FORBID)})")
syn = open(f"{P3}/code/q05_synthetic.py").read()
check(not re.search(r"read_parquet|read_csv|load_paths|np\.load|segments", syn.split('"""', 2)[-1]), "D the synthetic tests load no data file")
VAR_IDS = [v["id"] for v in PLAN["variants"]]
check(len(VAR_IDS) == len(set(VAR_IDS)), "D variant ids unique")
THR = {"H1": "thr_auc_drop", "H2": "thr_auc_drop", "H3": "h3_thr"}
PLAIN_TOKEN = {"H1": "thr_auc_drop", "H2": "thr_auc_drop", "H3": "thr_kappa_frac"}


def planned(name, pid):
    """A Phase 4 name the plan defines: a listed quantity, or a variant copy of one (bounds via __lo, __hi)."""
    base_ = name[:-4] if name.endswith(("__lo", "__hi")) else name
    if base_ in QN:
        return base_.startswith(pid.lower() + "_")
    m = re.fullmatch(r"(h[1-3])_(v(?:10|[1-9])[abc]?)_([a-z0-9_]+)", base_)
    return bool(m) and m.group(1) == pid.lower() and f"{m.group(1)}_{m.group(3)}" in QN


def parses(e):
    try:
        QR.parse(e); return True
    except (ValueError, SyntaxError):
        return False


for p in preds:
    for f in ("plain", "threshold_why", "rationale", "against", "direction", "magnitude", "precision", "rule", "testable", "testable_runtime", "satisfiable", "variants", "name"):
        check(f in p and p[f], f"D {p['id']} has {f}")
    for f in ("direction", "magnitude", "precision", "rule", "testable", "testable_runtime"):
        check(parses(p[f]), f"D {p['id']} {f} parses without literals")
    check(p["rule"] == f"{p['magnitude']} and {p['precision']}", f"D {p['id']} rule equals magnitude and precision")
    for f in ("direction", "magnitude", "precision", "testable_runtime"):
        for n in QR.names(p[f]):
            check(n in ALL or planned(n, p["id"]), f"D {p['id']} {f} name {n} exists or is planned for it")
    check(all(n in ALL for n in QR.names(p["testable"])), f"D {p['id']} testable names exist now")
    for s in p["satisfiable"]:
        check(parses(s["expr"]) and all(n in ALL for n in QR.names(s["expr"])), f"D {p['id']} bound parses and its names exist now")
    check("{{" + PLAIN_TOKEN[p["id"]] in p["plain"], f"D {p['id']} plain wording cites its threshold or threshold rule")
    check(THR[p["id"]] in QR.names(p["magnitude"]), f"D {p['id']} magnitude uses its threshold")
    check(QR.names(p["precision"]) == {f"{p['id'].lower()}_p_holm", "alpha_family"}, f"D {p['id']} precision is the Holm p against alpha_family")
    check("Test:" in p["plain"] and "Holm-adjusted" in p["plain"], f"D {p['id']} plain wording states the general claim, then the test and its level")
    for v in p["variants"]:
        check(v["id"] in VAR_IDS, f"D {p['id']} variant {v['id']} defined")
        check(parses(v["applicable"]) and parses(v["contradicts"]), f"D {p['id']} {v['id']} expressions parse without literals")
        check(all(n in ALL for n in QR.names(v["applicable"])), f"D {p['id']} {v['id']} applicability names exist now")
        code = v["id"].lower()
        cn = QR.names(v["contradicts"])
        for n in cn:
            check(n in ALL or (planned(n, p["id"]) and f"_{code}_" in n), f"D {p['id']} {v['id']} name {n} planned for this variant")
        check(any(n.endswith("__hi") for n in cn) and (THR[p["id"]] in cn or any(n.endswith("_thr") for n in cn)), f"D {p['id']} {v['id']} contradicts only when the interval's upper bound is below the threshold")
        check(not any(n.endswith("_p") or n.startswith("alpha") for n in cn), f"D {p['id']} {v['id']} verdict has no significance test")
check(all([v["id"].lower() for v in p["variants"]] == QF.VARIANTS_FOR[p["id"]] for p in preds), "D the variants of every prediction in the plan equal those in the frozen code")
used_variants = {v["id"] for p in preds for v in p["variants"]}
check(used_variants == set(VAR_IDS), "D every defined variant is used and every used variant is defined")
check(val("thr_auc_drop") > QF.AUC_NULL, "D the H1 and H2 threshold lies beyond the null")
check(0 < val("thr_kappa_frac") < 1 and QC.kappa_threshold(0.1, 0.8, val("thr_kappa_frac")) > 0.1, "D the H3 threshold rule lies beyond its null whenever the maximum does")
check([c["id"] for c in PLAN["taxonomy"]["classes"]] == list(QR.CLASSES), "D taxonomy classes equal the classifier's")
tally_names = re.findall(r"`(tally_[a-z_]+)`", PLAN["taxonomy"]["tally"])
check(set(tally_names) == {"tally_confirmed", "tally_confirmed_not_robust", "tally_failed", "tally_failed_direction", "tally_failed_magnitude", "tally_failed_precision", "tally_not_tested", "tally_total"}, "D tally names cover every class")
fm = PLAN["failure_modes"]
check(len(fm) == 3, "D the section Why this plan might fail lists the three most likely failure modes")
probs = [val(w["probability"]) for w in fm]
check(all(ALL[w["probability"]]["kind"] == "judgment" and 0 < val(w["probability"]) < 1 for w in fm), "D failure probabilities are judgments")
check(probs == sorted(probs, reverse=True), "D failure modes ordered most likely first")
check(val(PLAN["confidence"]["value"]) >= val(PLAN["confidence"]["target"]) or len(PLAN["confidence"]["raise"]) > 40, "D confidence below the target states what would raise it")
ex_ids = [e["id"] for e in PLAN["exploratory"]]
check(ex_ids == [f"E{i}" for i in range(1, len(ex_ids) + 1)], "D exploratory ids consecutive")
items = {i for t in PLAN["budget"]["tables"] + PLAN["budget"]["figures"] for i in t["items"]}
check(all(p["id"] in items for p in preds), "D every prediction appears in a table or figure")
check(all(e["id"] in items or e["output"].startswith("Text") for e in PLAN["exploratory"]), "D every exploratory analysis has an output")
check(all(m["why"] and m["alternatives"] for m in PLAN["methods"]), "D every method choice has a reason and alternatives")
check([m["id"] for m in PLAN["methods"]] == [f"M{i}" for i in range(1, len(PLAN["methods"]) + 1)], "D method ids consecutive")
check(all(a["test"] and a["action"] for a in PLAN["assumptions"]), "D every assumption has a test and an action")
check("Why this plan might fail" in open(f"{P3}/code/plan_template.md").read(), "D the plan has a section titled Why this plan might fail")
AUD = D["audit"]
check([a["id"] for a in AUD] == [f"SA{i}" for i in range(1, len(AUD) + 1)], "D self-audit ids consecutive")
check(all(a["disposition"] in ("addressed", "limitation", "deferred") and a["where"] for a in AUD), "D every self-audit item has a disposition and a place")
check({"Validity", "Novelty", "Impact", "Publishability"} <= {a["criterion"] for a in AUD}, "D the self-audit argues on validity, novelty, impact and publishability")
RV = D["review"]["items"]
check([x["id"] for x in RV] == [f"RV{i}" for i in range(1, len(RV) + 1)], "D review items numbered consecutively")
check(all(x["disposition"] in ("addressed", "addressed differently", "rejected") and x["change"] for x in RV), "D every review item has a disposition and a change")
rc_items = [i for c in D["review"]["root_cause"]["classes"] for i in c["items"]]
check(sorted(rc_items, key=lambda z: int(z[2:])) == [x["id"] for x in RV], "D the root-cause note assigns every review item to exactly one class")
rtext = D["review_text"]
check(rtext.count("**Major:") + rtext.count("**Minor:") == len(RV) and not any(c in rtext for c in "\u2013\u2014"), "D the verbatim report holds as many items as the dispositions, and no dash")
check("Forbidden quantities: I computed none" in rtext, "D the reviewer states it computed no confirmatory statistic")
check("both accelerometers and in Sound2" in PLAN["preregistration"].get("seen_note", ""), "D the Phase 2 sentence that reads as a joint result is flagged")

# ============================================================== E. satisfiability (C20)
for p in preds:
    check(QR.evaluate(p["testable"], ALL), f"E {p['id']} testable on the measured population")
    for s in p["satisfiable"]:
        check(QR.evaluate(s["expr"], ALL), f"E {p['id']} threshold bound holds: {s['expr']}")
na_pairs = {(p["id"], v["id"]) for p in preds for v in p["variants"] if not QR.evaluate(v["applicable"], ALL)}
declared = {(d["prediction"], d["variant"]) for d in PLAN["declared_not_applicable"]}
check(na_pairs == declared, f"E computed non-applicable variants equal the declared ones ({sorted(na_pairs)})")
for p in preds:
    check(any(QR.evaluate(v["applicable"], ALL) for v in p["variants"]), f"E {p['id']} keeps at least one applicable variant")

# ============================================================== F. machine-checkability dry run of the classifier
POP = ALL
EST = {"pass": 0.08, "short": 0.02, "wrong": -0.03}


def synth(p, case):
    h = p["id"].lower()
    s = {f"{h}_nonfinite": 0.0, f"{h}_p_holm": 0.5 if case == "FAILED_PRECISION" else 0.001}
    if p["id"] in ("H1", "H2"):
        e = EST["wrong" if case == "FAILED_DIRECTION" else "short" if case == "FAILED_MAGNITUDE" else "pass"]
        s.update({f"{h}_auc_run": 0.9, f"{h}_auc_family": 0.9 - e, f"{h}_drop": e})
        t = val("thr_auc_drop")
    else:
        nul, kmax = 0.1, 0.8
        t = QC.kappa_threshold(nul, kmax, val("thr_kappa_frac"))
        k = {"FAILED_DIRECTION": 0.05, "FAILED_MAGNITUDE": (nul + t) / 2}.get(case, (t + kmax) / 2)
        s.update({"h3_kappa": k, "h3_kappa_null": nul, "h3_kappa_max": kmax, "h3_thr": t})
    for v in p["variants"]:
        bad = case == "CONFIRMED_NOT_ROBUST" and v is p["variants"][0]
        for n in QR.names(v["contradicts"]):
            if n in ALL:
                continue
            s[n] = t if n.endswith("_thr") else (t * 0.5 if bad else t * 1.5)
    return s


for p in preds:
    for case in ("CONFIRMED", "CONFIRMED_NOT_ROBUST", "FAILED_DIRECTION", "FAILED_MAGNITUDE", "FAILED_PRECISION"):
        s = synth(p, case)
        got, _ = QR.classify(p, {**ALL, **s}, POP)
        check(got == case, f"F {p['id']} synthetic {case} classified as {got}")
        check(QR.evaluate(p["rule"], {**ALL, **s}) == (case in ("CONFIRMED", "CONFIRMED_NOT_ROBUST")), f"F {p['id']} rule truth matches the class for {case}")
    pop_bad = dict(POP)
    for n in QR.names(p["testable"]):
        if n.startswith("pop_"):
            pop_bad[n] = {**POP[n], "value": 0}
    check(QR.classify(p, {**ALL, **synth(p, "CONFIRMED")}, pop_bad)[0] == "NOT_TESTED", f"F {p['id']} untestable population gives NOT_TESTED")
    s = synth(p, "CONFIRMED"); s[f"{p['id'].lower()}_nonfinite"] = 0.5
    check(QR.classify(p, {**ALL, **s}, POP)[0] == "NOT_TESTED", f"F {p['id']} too many undefined draws give NOT_TESTED")
    s = synth(p, "CONFIRMED"); s[f"{p['id'].lower()}_p_holm"] = float("nan")
    check(QR.classify(p, {**ALL, **s}, POP)[0] == "NOT_TESTED", f"F {p['id']} an undefined p-value gives NOT_TESTED, never FAILED_PRECISION")
    s = synth(p, "CONFIRMED")
    s["h3_kappa" if p["id"] == "H3" else f"{p['id'].lower()}_drop"] = float("nan")
    check(QR.classify(p, {**ALL, **s}, POP)[0] == "NOT_TESTED", f"F {p['id']} an undefined estimate gives NOT_TESTED, never FAILED_DIRECTION")
    s = synth(p, "CONFIRMED")
    for n in list(s):
        if re.match(rf"{p['id'].lower()}_{p['variants'][0]['id'].lower()}_", n):
            del s[n]
    got, against = QR.classify(p, {**ALL, **s}, POP)
    check(got == "CONFIRMED" and not against, f"F {p['id']} a variant that was not computed does not contradict ({got})")
s = synth(preds[2], "CONFIRMED"); s["h3_kappa_max"] = s["h3_kappa_null"]
check(QR.classify(preds[2], {**ALL, **s}, POP)[0] == "NOT_TESTED", "F H3 with a degenerate null (maximum equal to the null) gives NOT_TESTED")
mix = {"H1": "CONFIRMED", "H2": "FAILED_MAGNITUDE", "H3": "CONFIRMED_NOT_ROBUST"}
S = dict(ALL)
for p in preds:
    S.update(synth(p, mix[p["id"]]))
tl, cls = QR.tally(preds, S, POP)
check(cls == mix, f"F mixed synthetic outcome classified per prediction ({cls})")
check(tl["CONFIRMED"] == 1 and tl["CONFIRMED_NOT_ROBUST"] == 1 and tl["FAILED"] == 1 and tl["NOT_TESTED"] == 0 and tl["TOTAL"] == 3, f"F tally counts the mixed outcome ({tl})")
check(sum(tl[c] for c in QR.CLASSES) == tl["TOTAL"], "F classes are exhaustive and exclusive in the tally")
try:
    QR.parse("h1_drop >= 0.05")
    check(False, "F a numeric literal in a rule is refused")
except ValueError:
    check(True, "F a numeric literal in a rule is refused")
check(QR.lookup("x_ci__hi", {"x_ci": {"value": None, "lo": 0.1, "hi": 0.3}}) == 0.3, "F an interval bound is read through the __hi suffix")

# ============================================================== S. frozen estimators on synthetic data
for ok, what in SY.run():
    check(ok, what)

# ============================================================== G. budget and structure against the venue limits
W = {val("v_fig_width_col_mm"), val("v_fig_width_page_mm")}
for f in PLAN["budget"]["figures"]:
    check(val(f["width"]) in W, f"G {f['id']} width is a venue width")
    check(val(f["height"]) <= val("v_fig_height_max_mm"), f"G {f['id']} height within the venue maximum")
check(val("v_font_min_pt") <= val("fig_font_pt") <= val("v_font_max_pt"), "G figure lettering within the venue range")
check(val("fig_line_pt") * 25.4 / 72 >= val("v_line_min_mm"), "G thinnest line at least the venue minimum")
check(val("v_keywords_min") <= val("keywords_planned") <= val("v_keywords_max"), "G planned keywords within the venue range")
check(val("structure_levels_planned") <= val("v_heading_levels_max"), "G planned heading depth within the venue maximum")
check([t["id"] for t in PLAN["budget"]["tables"]] == [f"Table {i}" for i in range(1, len(PLAN["budget"]["tables"]) + 1)], "G tables numbered with Arabic numerals in order")
check([f["id"] for f in PLAN["budget"]["figures"]] == [f"Fig. {i}" for i in range(1, len(PLAN["budget"]["figures"]) + 1)], "G figures numbered in order")
in_structure = {r for s in PLAN["structure"] for r in s["rules"]}
check(in_structure <= set(ids), "G every rule the structure cites exists")
missing_rules = [x["id"] for x in RULES if x["phase"] == 4 and x["id"] not in in_structure]
check(not missing_rules, f"G every Phase 4 rule is mapped to a part of the manuscript ({missing_rules})")
check(any("language model" in s["content"] for s in PLAN["structure"] if s["section"].endswith("Methods")), "G the Methods section documents language-model use (R19)")
allc = " ".join(s["content"] for s in PLAN["structure"])
check(all(f"Table {i}" in allc for i in range(1, len(PLAN["budget"]["tables"]) + 1)), "G every table is placed in the structure")
check(all(f"Fig. {i}" in allc for i in range(1, len(PLAN["budget"]["figures"]) + 1)), "G every figure is placed in the structure")

# ============================================================== H. rendering
extra = {"check_line": {"value": "placeholder", "unit": "text", "estimand": {"population": "q04_check.py", "filter": "", "statistic": "result line"}, "source": "q04_check.py", "kind": "text"},
         "plan_date": {"value": datetime.datetime.now(datetime.timezone.utc).date().isoformat(), "unit": "date", "estimand": {"population": "render", "filter": "", "statistic": "date of this render"}, "source": "q04_check.py", "kind": "text"}}
normal, rn = RD.render("normal", extra)
skeleton, rs = RD.render("skeleton", extra)
rv_normal, rrn = RD.render_review("normal")
rv_skel, _ = RD.render_review("skeleton")
check(not rn.missing and not rrn.missing, f"H every token resolves (missing: {sorted(set(rn.missing + rrn.missing))[:5]})")
for doc, txt in (("plan", normal), ("review record", rv_normal)):
    check("{{" not in txt and "}}" not in txt, f"H no unresolved marker in the {doc}")
    check("§" not in txt and "¤" not in txt, f"H no skeleton marker in the normal render of the {doc}")
judg = sorted(k for k in rn.used if ALL.get(k, {}).get("kind") == "judgment")
check(all(ALL[k]["estimand"]["population"] == "analyst judgment" for k in judg), "H every judgment used is labelled as a judgment")
PATTERNS = [
    r"imi_vm20i|imi_vmx30ui|tmf_vf10",
    r"AA 6061|AISI 1018|AISI 4140",
    r"\b(?:D10|D[1-9]|I0[1-9]|I1[0-6]|S[1-3]|U[1-4]|C[12]|F[12]|C(?:2[0-9]|3[0-2])|H[1-4]|V(?:10|[1-9])[abc]?|E(?:1[01]|[1-9])|M(?:1[01]|[1-9])|A[1-6]|W[1-3]|Q[1-5]|R(?:0[1-9]|[123][0-9])|SA(?:1[0-9]|[1-9])|RV(?:1[0-9]|[1-9]))\b",
    r"\b(?:Sound|Acc|acc|s)[0-2]\b",
    r"(?i)\blabels? [0-2](?: and [0-2])?\b",
    r"\bPhases? [1-5](?: and [1-5])?\b",
    r"\bFig\. [1-9]\b|\bTable [1-9]\b",
    r"\bsections? \d+(?:\.\d+)?\b",
    r"(?m)^#{1,4} \d+(?:\.\d+)?\.?",
    r"(?m)^\d+\. ",
    r"\bsha256\b",
    r"\bround [12]\b",
    r"CC BY 4\.0",
    r"\b[Tt]ype 1 research data policy\b",
    r"\bm/s2\b",
    r"\b[1-6] (?:Introduction|Dataset and populations|Methods|Results|Discussion|Conclusions)\b",
    r"\b[\w./-]+\.(?:md|csv|json|tsv|py|bst|parquet)\b",
]


def hand_digits(sk):
    sk = re.sub(r"`[^`]*`", " ", sk)
    for pat in PATTERNS:
        sk = re.sub(pat, " ", sk)
    return [(sk.count("\n", 0, m.start()) + 1, sk[max(0, m.start() - 50):m.end() + 30].replace("\n", " ")) for m in re.finditer(r"\d", sk)]


for doc, sk in (("plan", skeleton), ("review record", rv_skel)):
    left = hand_digits(sk)
    check(not left, f"H no hand-typed digit in the {doc} ({len(left)} found; first: {left[:6]})")

# ============================================================== I. language lint
p07_lint.ALLOW_PATH = f"{P3}/inputs/lint_allow3.json"
lerr, lwarn, lstats = p07_lint.lint(normal, skeleton)
check(not lerr, f"I language lint of the plan clean ({len(lerr)} errors; first: {lerr[:4]})")
rerr, rwarn, _ = p07_lint.lint(rv_normal, rv_skel)
check(not rerr, f"I language lint of the review record clean ({len(rerr)} errors; first: {rerr[:4]})")
DASH_CHARS = "\u2012\u2013\u2014\u2015"
for f in sorted(x for x in glob.glob(f"{P3}/code/*") + glob.glob(f"{P3}/inputs/*") if os.path.isfile(x)):
    txt = open(f, encoding="utf-8").read()
    check(not any(c in txt for c in DASH_CHARS), f"I no dash character in {os.path.basename(f)}")

# ============================================================== J. freeze
FILES = {"freeze_sha256_plan": f"{P3}/inputs/plan.json", "freeze_sha256_q_common": f"{P3}/code/q_common.py",
         "freeze_sha256_q_confirm": f"{P3}/code/q_confirm.py", "freeze_sha256_q_rules": f"{P3}/code/q_rules.py"}
for k, path in FILES.items():
    h = hashlib.sha256(open(path, "rb").read()).hexdigest()
    check(k in V3 and V3[k]["value"] == h, f"J {k} recorded and equal to the file on disk")

# ============================================================== K. result, final renders, files on disk
N_POST = 6
n_total = N + N_POST
status = "PASS" if not FAIL else "FAIL"
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
line = f"{status}, {n_total:,} assertions run by q04_check.py at {stamp}" + ("" if not FAIL else f"; {len(FAIL)} failed")
extra["check_line"]["value"] = line
final, rf = RD.render("normal", extra)
open(f"{P3}/outputs/phase3_plan.md", "w").write(final)
rv_final, _ = RD.render_review("normal")
open(f"{P3}/outputs/review3_round1.md", "w").write(rv_final)
disk = open(f"{P3}/outputs/phase3_plan.md").read()
rdisk = open(f"{P3}/outputs/review3_round1.md").read()
check(f"**Check: {line}**" in disk, "K the plan on disk leads with this check result")
check(disk == final and not rf.missing, "K the plan on disk equals a fresh render with no missing token")
check("check not yet run" not in disk, "K no placeholder check line")
check(disk.index("**Check:") < disk.index("## 1. Negative findings first"), "K the check result comes before the findings")
check(rdisk == rv_final, "K the review record on disk equals a fresh render")
check(D["review_text"].strip() in rdisk, "K the review record carries the reviewer's report verbatim")
assert N == n_total, "the advance count of group K assertions is wrong"
status = "PASS" if not FAIL else "FAIL"
if status == "FAIL" and "FAIL" not in line:
    line = f"FAIL, {N:,} assertions run by q04_check.py at {stamp}; {len(FAIL)} failed"
    extra["check_line"]["value"] = line
    open(f"{P3}/outputs/phase3_plan.md", "w").write(RD.render("normal", extra)[0])
v2_prefixes = sorted({k.split("_")[0] + "_" for k in V2})
res = {"status": status, "assertions": N, "failed": FAIL, "time_utc": stamp, "check_line": line,
       "judgments_used_in_plan": judg, "lint_warnings": [list(w) for w in lwarn], "sentence_stats": lstats,
       "values_used_in_plan": len(rn.used), "values3_in_file": len(V3), "phase2_value_prefixes_seen": v2_prefixes}
json.dump(res, open(f"{P3}/outputs/check3_result.json", "w"), indent=1)
print(line)
for f in FAIL:
    print("FAILED:", f)
print(f"lint warnings for a human pass: {len(lwarn)} (plan), {len(rwarn)} (review record)")
sys.exit(0 if status == "PASS" else 1)
