"""r10_check.py: the Phase 4 check. Asserts the freeze, computes the tally, builds the manuscript and tests it.

Groups
  A  freeze of the Phase 3 plan and estimator code; variant settings against the registered judgments
  B  values files: required fields, finiteness, intervals with the estimand of their point, planned names
  C  recomputation of the primary confirmatory statistics by the frozen functions (no bootstrap), Holm, testability
  D  population values that the testability and applicability expressions read, re-measured from the data
  E  classification and tally (r08_tally.py) and their use in the text
  F  statements in the text that describe values (direction words, 'every', 'none', ties)
  G  manuscript build: compilation, references, citation order, DOIs
  H  no typed numeral in the prose of the template
  I  language lint (r06_lint.py): no error
  J  venue checklist rules enforced by script (Phase 3 plan, section 3)
  K  figure files: size, fonts, lettering, lines, colour model, names, inspection record
  L  Online Resources (ESM_2.zip included), deviations log, self-audit, review records, open items
  M  analyses added after the review (post hoc), recomputed from the data and the saved arrays
Writes outputs/check4_result.json and the measured manuscript counts (words_*) into values4.json.
"""
import sys, json, re, os, glob, hashlib, datetime, subprocess
sys.path.insert(0, "/home/claude/mill/phase4/code")
sys.path.insert(0, "/home/claude/mill/phase3/code")
sys.path.insert(0, "/home/claude/mill/code")
import r_freeze
FREEZE = r_freeze.assert_freeze()

import numpy as np
import pandas as pd
from vals import Values
import q_common as QC
import q_confirm as QF
import q_rules as QR
import r08_tally
import r07_build as RB
import r06_lint as RL

P2, P3, P4 = "/home/claude/mill", "/home/claude/mill/phase3", "/home/claude/mill/phase4"
RESULTS = []
# --prepackage: the run that r12_package.py needs before it can build ESM_2.zip; it skips only the assertions
# about the archive itself, which the final run (without the flag) makes, including that the archived check
# result is a passing pre-packaging run
PREPACK = "--prepackage" in sys.argv


def ok(cond, group, msg):
    RESULTS.append((bool(cond), group, msg))
    return bool(cond)


def close(a, b, tol=1e-9):
    return a is not None and b is not None and abs(float(a) - float(b)) <= tol


# ------------------------------------------------------------------ A. freeze
V3 = json.load(open(f"{P3}/outputs/values3.json"))
V2 = json.load(open(f"{P2}/outputs/values.json"))
for name, path in r_freeze.FROZEN.items():
    ok(r_freeze.sha256(path) == V3[name]["value"], "A", f"{os.path.basename(path)} has its frozen sha256")
g23 = lambda n: (V3[n] if n in V3 else V2[n])["value"]
ok(QF.VARIANTS["v6a"]["comb_half_bins"] == g23("comb_bins_v6a"), "A", "V6a setting equals comb_bins_v6a")
ok(QF.VARIANTS["v6b"]["comb_half_bins"] == g23("comb_bins_v6b"), "A", "V6b setting equals comb_bins_v6b")
ok(QF.VARIANTS["v6c"]["rel_speed_tol"] == g23("rel_speed_tol"), "A", "V6c setting equals rel_speed_tol")
ok(QF.VARIANTS["v7"]["band"] == (g23("d5_band_lo_hz"), g23("band_hi_v7_hz")), "A", "V7 band equals d5_band_lo_hz to band_hi_v7_hz")
ok(QC.SEED == g23("seed_analysis"), "A", "analysis seed equals seed_analysis")
for script in sorted(glob.glob(f"{P4}/code/r[01]*.py")):
    src = open(script).read()
    if os.path.basename(script) in ("r_freeze.py",):
        continue
    ok("r_freeze.assert_freeze()" in src or os.path.basename(script) in ("r06_lint.py", "r07_build.py"), "A", f"{os.path.basename(script)} asserts the freeze before computing")

# ------------------------------------------------------------------ E (first half). classification and tally, written before the build
classes, counts = r08_tally.run()
V4 = json.load(open(f"{P4}/outputs/values4.json"))
ALL = {**V2, **V3, **V4}
POPVALS = {**V2, **V3}
PLAN = json.load(open(f"{P3}/inputs/plan.json"))

# ------------------------------------------------------------------ B. values
REQ = ("value", "unit", "estimand", "source")
for k, r in V4.items():
    ok(all(f in r for f in REQ), "B", f"{k} has value, unit, estimand and source")
    if r.get("kind") == "interval":
        ok(r["point"] in V4 and V4[r["point"]]["estimand"] == r["estimand"], "B", f"interval {k} carries the estimand of its point {r['point']}")
        ok(r["lo"] <= r["hi"], "B", f"interval {k} has lo <= hi")
    elif isinstance(r["value"], float):
        ok(np.isfinite(r["value"]), "B", f"{k} is finite")
    ok(r["source"] in {os.path.basename(p) for p in glob.glob(f"{P4}/code/*.py")}, "B", f"{k} was written by a Phase 4 script ({r['source']})")
    ok(set(r["estimand"]) >= {"population", "filter", "statistic"}, "B", f"{k} records population, filter and statistic")
for name, q in PLAN["quantities"].items():
    ok(name in V4, "B", f"planned quantity {name} exists")
    if name in V4 and not name.endswith("_ci"):
        ok(V4[name]["estimand"]["filter"] == q["filter"], "B", f"{name} carries the plan's filter")
        ok(V4[name]["estimand"]["statistic"].startswith(q["statistic"]), "B", f"{name} carries the plan's statistic")
    if name.endswith("_ci") and name in V4:
        ok(V4[name]["method"].startswith("percentile interval at ci_level over boot_b"), "B", f"{name} records its resampling method")
for pred in PLAN["predictions"]:
    for v in pred["variants"]:
        for nm in QR.names(v["contradicts"]):
            base = nm[:-4] if nm.endswith(("__lo", "__hi")) else nm
            ok(base in ALL, "B", f"variant value {base} of {pred['id']} {v['id']} exists")
names_by_file = [set(json.load(open(p))) for p in (f"{P2}/outputs/values.json", f"{P3}/outputs/values3.json", f"{P4}/outputs/values4.json")]
ok(not (names_by_file[0] & names_by_file[1]) and not (names_by_file[0] & names_by_file[2]) and not (names_by_file[1] & names_by_file[2]), "B", "no value name is defined in two values files")

# ------------------------------------------------------------------ C. recomputation of the primary statistics (no bootstrap)
paths = QC.load_paths()
y = paths.y.values
for H, fset in (("h1", "highrate"), ("h2", "mtc")):
    S = QC.oof_run_matrix(paths, fset, int(g23("s1_repeats")), int(g23("s1_folds")), "lr", float(g23("lr_c")), False, True, int(g23("seed_analysis")))
    sf = QC.oof_family(paths, fset, "lr", float(g23("lr_c")), False, True)
    ar, af = QC.auc_over_repeats(y, S), QC.auc_w(y, sf)
    ok(close(ar, V4[f"{H}_auc_run"]["value"], 1e-12), "C", f"{H}_auc_run recomputed ({ar:.6f})")
    ok(close(af, V4[f"{H}_auc_family"]["value"], 1e-12), "C", f"{H}_auc_family recomputed ({af:.6f})")
    ok(close(ar - af, V4[f"{H}_drop"]["value"], 1e-12), "C", f"{H}_drop equals the difference of its AUCs")
    ok(V4[f"{H}_n_paths"]["value"] == len(paths) and V4[f"{H}_n_runs"]["value"] == paths.run_id.nunique(), "C", f"{H} population counts")
lab1 = paths[paths.y == 1].reset_index(drop=True)
ia, ib = QC.nh_indicator(lab1, "acc0"), QC.nh_indicator(lab1, "s2")
arr = np.load(f"{P4}/outputs/r01_arrays.npz", allow_pickle=True)
ok(np.array_equal(ia, arr["h3_acc0"], equal_nan=True) and np.array_equal(ib, arr["h3_s2"], equal_nan=True), "C", "H3 indicators recomputed from the stored spectra")
keep = np.isfinite(ia) & np.isfinite(ib)
strata = QF.h3_strata(lab1, g23("speed_class_edge_rpm"))
k0 = QC.kappa_w(ia[keep].astype(int), ib[keep].astype(int))
kn = QC.kappa_null_w(ia[keep].astype(int), ib[keep].astype(int), strata[keep])
km = QC.kappa_max_w(ia[keep].astype(int), ib[keep].astype(int), strata[keep])
ok(close(k0, V4["h3_kappa"]["value"], 1e-12), "C", f"h3_kappa recomputed ({k0:.6f})")
ok(close(kn, V4["h3_kappa_null"]["value"], 1e-12), "C", "h3_kappa_null recomputed")
ok(close(km, V4["h3_kappa_max"]["value"], 1e-12), "C", "h3_kappa_max recomputed")
ok(close(QC.kappa_threshold(kn, km, g23("thr_kappa_frac")), V4["h3_thr"]["value"], 1e-12), "C", "h3_thr follows the registered rule")
ok(close(k0 - kn, V4["h3_excess"]["value"], 1e-12), "C", "h3_excess equals kappa minus its null")
tab = {c: sum(V4[f"h3_tab_{c}_{x}"]["value"] for x in ("both", "acc0_only", "s2_only", "neither")) for c in ("lo", "hi")}
ok(tab["lo"] + tab["hi"] == V4["h3_n_paths"]["value"], "C", "the H3 cross-tabulation covers every H3 path")
ok(tab["lo"] == V3["pop_h3_primary_lo_n"]["value"] and tab["hi"] == V3["pop_h3_primary_hi_n"]["value"], "C", "speed-class counts equal the Phase 3 population values")
pv = {p["id"]: V4[f"{p['id'].lower()}_p"]["value"] for p in PLAN["predictions"]}
tested = {p["id"] for p in PLAN["predictions"] if QR.testable(p, ALL, POPVALS)[0]}
adj = QF.holm_adjust(pv, tested)
for pid, a in adj.items():
    ok(close(a, V4[f"{pid.lower()}_p_holm"]["value"], 1e-12), "C", f"{pid} Holm-adjusted p recomputed")
ok(len(tested) == V4["holm_n_tested"]["value"], "C", "holm_n_tested equals the number of testable predictions")
B = int(g23("boot_b"))
for pid in ("H1", "H2", "H3"):
    p = V4[f"{pid.lower()}_p"]["value"]
    ok(abs(p * (B + 1) - round(p * (B + 1))) < 1e-6, "C", f"{pid} p-value is a count over boot_b plus one")
    ok(V4[f"{pid.lower()}_nonfinite"]["value"] <= g23("max_nonfinite_share"), "C", f"{pid} undefined draws within the registered share")

# ------------------------------------------------------------------ D. population values re-measured
def kish(c):
    c = np.asarray(c, float)
    return float(c.sum() ** 2 / (c ** 2).sum())


fams = sorted(paths.family.unique())
for fset, tag in (("highrate", "h_highrate_primary"), ("mtc", "h_mtc_primary")):
    d = paths.dropna(subset=QC.FEATS[fset])
    ok(min(int(((d.family != f) & (d.y == 1)).sum()) for f in fams) == V3[f"pop_{tag}_min_train_l1_family"]["value"], "D", f"pop_{tag}_min_train_l1_family re-measured")
    ok(min(int(((d.family != f) & (d.y == 0)).sum()) for f in fams) == V3[f"pop_{tag}_min_train_l0_family"]["value"], "D", f"pop_{tag}_min_train_l0_family re-measured")
    rng = np.random.default_rng(QC.SEED); worst = 10 ** 9
    for _ in range(int(g23("s1_repeats"))):
        fo = QC.run_folds(d.run_id.values, int(g23("s1_folds")), rng)
        worst = min(worst, min(int((d.y.values[fo != f] == 1).sum()) for f in range(int(g23("s1_folds")))))
    ok(worst == V3[f"pop_{tag}_min_train_l1_run"]["value"], "D", f"pop_{tag}_min_train_l1_run re-measured")
ok(close(kish(paths[paths.y == 1].groupby("run").size()), V3["pop_kish_l1_runs_primary"]["value"], 1e-12), "D", "pop_kish_l1_runs_primary re-measured")
for cls, m in (("lo", lab1.N < g23("speed_class_edge_rpm")), ("hi", lab1.N >= g23("speed_class_edge_rpm"))):
    ok(int(m.sum()) == V3[f"pop_h3_primary_{cls}_n"]["value"], "D", f"pop_h3_primary_{cls}_n re-measured")
    ok(int(lab1[m].run.nunique()) == V3[f"pop_h3_primary_{cls}_runs"]["value"], "D", f"pop_h3_primary_{cls}_runs re-measured")

# ------------------------------------------------------------------ E. tally
ok(counts["TOTAL"] == len(PLAN["predictions"]), "E", "tally_total equals the number of predictions")
ok(sum(counts[c] for c in QR.CLASSES) == counts["TOTAL"], "E", "the classes add up to the total")
ok(counts["FAILED"] == sum(counts[c] for c in QR.FAILED), "E", "tally_failed is the sum of its parts")
for pid, (cls, against) in classes.items():
    ok(V4[f"{pid.lower()}_class"]["value"] == cls, "E", f"{pid} class written as computed ({cls})")
    r = {"direction": QR.evaluate(next(p for p in PLAN['predictions'] if p['id'] == pid)["direction"], ALL),
         "magnitude": QR.evaluate(next(p for p in PLAN['predictions'] if p['id'] == pid)["magnitude"], ALL),
         "precision": QR.evaluate(next(p for p in PLAN['predictions'] if p['id'] == pid)["precision"], ALL)}
    expect = ("FAILED_DIRECTION" if not r["direction"] else "FAILED_MAGNITUDE" if not r["magnitude"] else "FAILED_PRECISION" if not r["precision"]
              else ("CONFIRMED_NOT_ROBUST" if against else "CONFIRMED"))
    ok(cls == expect or cls == "NOT_TESTED", "E", f"{pid} class agrees with its rule parts evaluated separately")

# ------------------------------------------------------------------ G. build the manuscript
b = RB.build()
tex, skel = b["tex"], b["skeleton"]
BUILD = RB.BUILD
ok(all(rc == 0 for _, rc in b["compile"][1:2]) and os.path.exists(f"{BUILD}/manuscript.pdf"), "G", "bibtex ran clean and the PDF exists")
log = open(f"{BUILD}/manuscript.log", errors="replace").read()
ok(not re.search(r"^! ", log, flags=re.M), "G", "no LaTeX error in the log")
ok(not re.search(r"Citation .* undefined", log), "G", "no undefined citation")
ok(not re.search(r"Reference .* undefined", log), "G", "no undefined reference")
ok(not re.search(r"There were undefined references", log), "G", "LaTeX reports no undefined references")
over = [float(x) for x in re.findall(r"Overfull \\hbox \(([\d.]+)pt too wide\)", log)]
ok(all(o < 1.0 for o in over), "G", f"no overfull box wider than 1 pt ({len(over)} overfull boxes, largest {max(over) if over else 0:.1f} pt)")
pages = int(re.search(r"Pages:\s+(\d+)", subprocess.run(["pdfinfo", f"{BUILD}/manuscript.pdf"], capture_output=True, text=True).stdout).group(1))
ok(pages > 0, "G", f"the PDF has {pages} pages")
refs = json.load(open(f"{P4}/inputs/references.json"))
keys = [r["key"] for r in refs["references"]]
cited = []
for m in re.finditer(r"\\cite\{([^}]*)\}", tex):
    for k in m.group(1).split(","):
        if k.strip() not in cited:
            cited.append(k.strip())
ok(set(cited) == set(keys), "G", f"every reference is cited and every citation is listed ({len(keys)} references)")
bbl = open(f"{BUILD}/manuscript.bbl").read()
order = re.findall(r"\\bibitem\[[^\]]*\]\{([^}]*)\}", bbl.replace("\n", " "))
ok(order == cited, "G", "the reference list is numbered in order of first citation")
for r in refs["references"]:
    ok(r.get("doi") or r.get("url"), "G", f"R08 {r['key']} has a DOI or a stable URL")
    ok(r.get("tag") == "VERIFIED" and r.get("retrieved_utc") and r.get("used_for"), "G", f"{r['key']} was read for what the text uses it for (VERIFIED), with retrieval and use recorded")
ok("\\providecommand{\\doi}[1]{\\url{https://doi.org/#1}}" in bbl, "G", "DOIs print as full https://doi.org links")
ok(len(set(keys)) == len(keys), "G", "reference keys are unique")
ok("hendriks2022" not in keys and "hendriks2022" in refs.get("dropped", {}), "G", "the title-only reference hendriks2022 is dropped, with its reason recorded (review item R13)")
bibt = open(f"{BUILD}/refs.bib").read()
ok("jstor.org/stable/4615733" in bibt and "jstor.org/stable/4615733" in bbl, "G", "R08 Holm (1979) carries its JSTOR stable link in the bibliography")
# every table note and marker is in the compiled PDF (review item R8)
pdftxt = subprocess.run(["pdftotext", f"{BUILD}/manuscript.pdf", "-"], capture_output=True, text=True).stdout
NORM = lambda x: re.sub(r"[^a-z0-9]", "", x.lower())
flat_pdf = NORM(pdftxt)
notes = re.findall(r"\\tabnote\{((?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)\}", tex)
ok(len(notes) >= 8, "G", f"table notes found in the source ({len(notes)})")
for nt in notes:
    words = re.sub(r"\s+", " ", RL.detex("\\begin{document}" + re.sub(r"\\textsuperscript\{[^}]*\}", " ", nt))).strip().split(" ")
    probe = " ".join(words[:8])
    ok(probe and NORM(probe) in flat_pdf, "G", f"R16 table note printed in the PDF: {probe[:50]}")
tables = re.findall(r"\\begin\{table\*?\}.*?\\end\{table\*?\}", tex, flags=re.S)
marks = [m for t in tables for m in re.findall(r"\\textsuperscript\{([^}]*)\}", t)]
ok(marks and all(re.fullmatch(r"[a-z]|\*", m) for m in marks), "G", f"R16 table note markers are lower-case letters or an asterisk ({sorted(set(marks))})")
tex_nc = re.sub(r"(?m)(?<!\\)%.*$", "", tex)
ok("\\footnotetext" not in tex_nc and "\\footnotemark" not in tex_nc, "G", "no footnote command left in a float, where full-width tables lose it")
ok(flat_pdf.count(NORM("The whole interval lies below the threshold")) == 1, "G", "the Table 4 asterisk is explained once")

# ------------------------------------------------------------------ H. no typed numeral in the prose of the template
tpl, parts = RB.template()
bodyt = tpl[tpl.index("\\begin{document}"):]
scan = RB.TOKEN.sub(" ", bodyt)
scan = re.sub(r"(?m)^%.*$", "", scan)
scan = re.sub(r"\\begin\{equation\}.*?\\end\{equation\}", " ", scan, flags=re.S)
scan = re.sub(r"\$[^$]*\$", " ", scan)
scan = re.sub(r"\\(label|ref|cite|includegraphics|url|typeout|footnotemark|footnotetext)(\[[^\]]*\])?(\{[^}]*\})?", " ", scan)
scan = re.sub(r"\\(author\*?|affil\*?)\[[^\]]*\]", " ", scan)
scan = re.sub(r"\\begin\{tabular\*?\}(\{[^}]*\})?\{[^}]*\}", " ", scan)
scan = re.sub(r"\\(multicolumn)\{\d+\}", " ", scan)
scan = re.sub(r"\\cmidrule\{[^}]*\}", " ", scan)
scan = re.sub(r"\\email\{[^}]*\}", " ", scan)
IDENT = [r"(?i)\blabels? [012]\b", r"(?i)\blabels 0 and 1\b", r"(?i)\blabel 0 or label 1\b", r"\bH[123]\b", r"\bV(?:[1-9]|6[abc])\b", r"\bE(?:1[01]|[1-9])\b",
         r"\b[CF][12]\b", r"\bOnline Resources? [1-4](?:, [1-4])*(?: and [1-4])?\b", r"\bESM\\_[1-4]\.(?:csv|zip)\b"]
scan2 = scan
for pat in IDENT:
    scan2 = re.sub(pat, " ", scan2)
digits = [(m.group(0), scan2[max(0, m.start() - 30):m.end() + 30].replace("\n", " ")) for m in re.finditer(r"\d+", scan2)]
ok(not digits, "H", f"no typed numeral in the prose ({digits[:3]})")

# ------------------------------------------------------------------ I. language lint
errors, warnings, stats = RL.lint(tex, skel)
ok(not errors, "I", f"language lint: {len(errors)} errors ({errors[:2]})")

# ------------------------------------------------------------------ J. venue checklist rules
V = Values(path=f"{P4}/outputs/values4.json", script="r10_check.py")
title = RL.title_of(tex)
ok(title and len(title.split()) <= 25, "J", f"R02 title present ({len(title.split())} words)")
TOPT = json.load(open(f"{P4}/inputs/title_options4.json"))
ok(len(TOPT["options"]) == 3 and len({o["framing"] for o in TOPT["options"]}) == 3, "J", "three title options with different framings, as the prompt asks")
ok(title == next(o["title"] for o in TOPT["options"] if o["id"] == TOPT["chosen"]), "J", "the manuscript carries the chosen title option")
wmin, wmax = ALL["title_study_words_min"]["value"], ALL["title_study_words_max"]["value"]
for o in TOPT["options"]:
    t_ = o["title"]
    ok(not any(w in t_.lower() for w in RL.TITLE_BANNED) and not re.match(r"^\w+ing\b", t_) and "?" not in t_ and "--" not in t_
       and not any(ch in t_ for ch in RL.DASHES) and ":" not in t_ and wmin <= len(t_.split()) <= wmax,
       "J", f"title option {o['id']} keeps the prompt's rules (no claim word, gerund opener, question, dash or colon; length within the sampled IJAMT range)")

abstract = re.search(r"%%ABSTRACT-START\n\\abstract\{(.*?)\}\n%%ABSTRACT-END", tex, flags=re.S).group(1)
abs_plain = RL.detex("\\begin{document}" + abstract)
n_abs = len(abs_plain.split())
ok(150 <= n_abs <= 250, "J", f"R03 abstract has 150 to 250 words ({n_abs})")
ok("\\cite" not in abstract, "J", "R03 no citation in the abstract")
for ab in sorted(set(re.findall(r"\b([A-Z]{2,})\b", abs_plain))):
    ok(ab in ("MTConnect",) or re.search(r"\(" + ab + r"\)", abs_plain), "J", f"R03 abbreviation {ab} is defined in the abstract")
kw = re.search(r"\\keywords\{([^}]*)\}", tex).group(1).split(",")
ok(4 <= len(kw) <= 6 and len(kw) == g23("keywords_planned"), "J", f"R04 {len(kw)} keywords")
depth = 3 if "\\subsubsection" in bodyt else 2 if "\\subsection" in bodyt else 1
ok(depth <= 3 and depth == g23("structure_levels_planned"), "J", f"R05 heading depth {depth}")
main = tex[tex.index("\\section{Introduction}"):tex.index("\\backmatter")]
main_plain = RL.detex("\\begin{document}" + re.sub(r"\\begin\{(table|figure)\*?\}.*?\\end\{(table|figure)\*?\}", " ", main, flags=re.S))
for ab, long in (("MSM", "multi-sensor and MTConnect"), ("CNC", "computer numerical control"), ("RMS", "root-mean-square"), ("AUC", "area under the receiver operating characteristic curve")):
    first_def = main_plain.find(f"({ab})")
    first_use = min([m.start() for m in re.finditer(r"\b" + ab + r"\b", main_plain)] or [10 ** 9])
    ok(first_def >= 0 and first_use >= first_def - 1 and long in main_plain[:first_def + 5], "J", f"R06 {ab} defined at its first use in the main text")
ok(not re.search(r"\brpm\b|\bRPM\b|\bsec\b|\bmsec\b", main_plain), "J", "R07 no banned unit spelling")
n_words = len(main_plain.split())
V.set("words_main", n_words, "words", {"population": "manuscript", "filter": "Introduction to Conclusions, tables, figures and captions removed", "statistic": "words of the rendered text"})
V.set("words_abstract", n_abs, "words", {"population": "manuscript", "filter": "abstract", "statistic": "words of the rendered text"})
V.set("n_references", len(keys), "references", {"population": "manuscript", "filter": "reference list", "statistic": "entries"})
ijamt = sum(1 for r in refs["references"] if r.get("journal") == "Int J Adv Manuf Technol")
V.set("n_references_ijamt", ijamt, "references", {"population": "manuscript", "filter": "reference list", "statistic": "entries published in IJAMT"})
V.save()
ok(n_words <= g23("words_main_max"), "J", f"R27 main text within words_main_max ({n_words})")
ok(ijamt / len(keys) <= 0.15, "J", f"R09 at most 15 percent of the entries from IJAMT ({ijamt} of {len(keys)})")
own = {(a["family"], a["given"][0]) for a in json.load(open(f"{P4}/inputs/authors.json"))["authors"]}
refauth = {(a.split(",")[0].strip(), a.split(",")[1].strip()[:1]) for r in refs["references"] for a in r["author"]}
ok(not (own & refauth), "J", "R09 no reference carries an author of this paper (no self-citation)")
flat = [p for p in os.listdir(BUILD) if os.path.isdir(os.path.join(BUILD, p))]
ok(not flat, "J", "R10 the submission folder has no subfolder")
TPLD = f"{P4}/template/sn/sn-article-template"
ok(r_freeze.sha256(f"{BUILD}/sn-jnl.cls") == r_freeze.sha256(f"{TPLD}/sn-jnl.cls"), "J", "R10 the class file is the template's own, unchanged")
a_bst = open(f"{TPLD}/bst/sn-basic.bst", encoding="latin-1").read().split("\n")
b_bst = open(f"{BUILD}/sn-basic.bst", encoding="latin-1").read().split("\n")
diff = [(x, y2) for x, y2 in zip(a_bst, b_bst) if x != y2]
ok(len(a_bst) == len(b_bst) and diff == [("SORT", "%SORT"), ("SORT", "%SORT")], "J", "R10 sn-basic.bst differs from the template's only in the two SORT lines")
ok(r_freeze.sha256(f"{P4}/template/sn_template_dec2024.zip") == json.load(open(f"{P4}/inputs/sources4.json"))["template"]["sha256"] and os.path.getsize(f"{P4}/template/sn_template_dec2024.zip") == 901814, "J", "R10 the template package is the recorded download")
ok("\\documentclass[pdflatex,sn-basic,Numbered,iicol]{sn-jnl}" in tex, "J", "R10 the class options are sn-basic, Numbered and iicol")
for i in range(1, 6):
    ok(os.path.exists(f"{BUILD}/Fig{i}.eps"), "J", f"R13 Fig{i}.eps is in the submission folder")
caps = re.findall(r"\\caption\{((?:[^{}]|\{[^{}]*\})*)\}", tex)
for c in caps:
    ok(not re.search(r"[.,;:]\s*$", c.strip()), "J", f"R14 caption ends without punctuation: {c[:40]}")
    ok(not re.search(r"\b(red|blue|green|orange|colour|color)\b", c, flags=re.I), "J", f"R15 caption does not rely on colour: {c[:40]}")
figenv = re.findall(r"\\label\{(fig:[^}]*)\}", tex)
firstref = []
for m in re.finditer(r"\\ref\{(fig:[^}]*)\}", tex):
    if m.group(1) not in firstref:
        firstref.append(m.group(1))
ok(firstref == figenv, "J", "R14 figures are cited in consecutive order and numbered in that order")
tabenv = re.findall(r"\\label\{(tab:[^}]*)\}", tex)
firsttab = []
for m in re.finditer(r"\\ref\{(tab:[^}]*)\}", tex):
    if m.group(1) not in firsttab:
        firsttab.append(m.group(1))
ok(firsttab == tabenv, "J", "R16 tables are cited in consecutive order and numbered in that order")
ok(tex.index("\\bibliography{refs}") < tex.index("\\section*{Statements and Declarations}"), "J", "R17 Statements and Declarations follow the references (Q2)")
decl = tex[tex.index("\\section*{Statements and Declarations}"):]
for head in ("Funding", "Competing interests", "Ethics approval and consent to participate", "Consent for publication", "Data availability", "Code availability", "Author contributions"):
    ok(f"\\bmhead{{{head}}}" in decl, "J", f"R17 declaration heading {head}")
ok("Use of a language model" in tex or "use of a language model" in tex, "J", "R19 the Methods document the use of a language model")
# the corresponding author's instructions: the language-model statement and the responsibility sentence form one
# paragraph of their own, and every mention of language-model use is set in red (\llm, a red \textcolor)
body_nc = re.sub(r"(?m)(?<!\\)%.*$", "", tex[tex.index("\\begin{document}"):])
rest = re.sub(r"\\llm\{(?:[^{}]|\{[^{}]*\})*\}", " ", body_nc)
rest = re.sub(r"\\texorpdfstring\{[^{}]*\}\{[^{}]*\}", " ", rest)
rest = re.sub(r"\\(label|ref)\{[^}]*\}", " ", rest)   # cross-reference keys such as sec:llm are not text
ok(not re.search(r"(?i)language model|\bLLM\b|Claude|Anthropic", rest), "J", "every mention of language-model use is inside a red span")
ok("\\newcommand{\\llm}[1]{\\textcolor{red}{#1}}" in tex, "J", "the red span is a red \\textcolor")
i0 = body_nc.index("\\label{sec:llm}") + len("\\label{sec:llm}")
nxt = min(j for j in (body_nc.find("\\section", i0), body_nc.find("\\subsection", i0), body_nc.find("\\backmatter", i0)) if j > 0)
paras = [q.strip() for q in re.split(r"\n\s*\n", body_nc[i0:nxt]) if q.strip()]
ok(len(paras) == 1 and paras[0].startswith("\\llm{") and "large language model" in paras[0] and "The authors take responsibility for the final text." in paras[0],
   "J", "the language-model statement and the responsibility sentence form a single paragraph of their own subsection")
i1 = body_nc.index("\\label{sec:software}")
sw = body_nc[i1:body_nc.find("\\subsection", i1)]
ok("language model" not in sw.lower() and "claude" not in sw.lower(), "J", "the software paragraph does not mix in the language-model statement")
ok(NORM("The authors take responsibility for the final text.") in flat_pdf and NORM("Use of a language model") in flat_pdf, "J", "the language-model subsection prints in the PDF")
ok("\\cite{kim2026msm,kim2025kaggle}" in decl and "\\ccby" in decl, "J", "R20 data availability cites the dataset with its licence")
for i in (1, 2, 3, 4):
    ok(re.search(rf"Online Resource {i}\b", tex) and f"ESM\\_{i}" in tex, "J", f"R21 ESM_{i} is cited as Online Resource {i}")
ok("HBAF" not in tex, "J", "no unexpanded acronym for the U3 repository (review item R12)")
ok("pre-registered" not in tex.lower() and "pre-registration" not in tex.lower(), "J", "the manuscript says pre-specified, not pre-registered (DV18)")
OPEN = json.load(open(f"{P4}/inputs/open_items4.json"))
fund = decl[decl.index("\\bmhead{Funding}"):decl.index("\\bmhead{Competing interests}")]
ph = next((it for it in OPEN["items"] if it["id"] == "funding"), None)
ok(("To be confirmed" not in fund) or (ph is not None and ph["placeholder"] in fund), "J", "R17 the funding statement is given or its placeholder is carried as an open item for the authors")
QQ = json.load(open(f"{P3}/inputs/venue_checklist_quotes.json"))["quotes"]
ok(QQ["q_funding_none_example"] in fund, "J", "R17 the funding statement uses the journal's own wording for no funding (the authors received none)")
authors = json.load(open(f"{P4}/inputs/authors.json"))
order_tex = re.findall(r"\\author\*?\[[^\]]*\]\{\\fnm\{([^}]*)\} \\sur\{([^}]*)\}\}", tex)
ok([f"{a} {b2}" for a, b2 in order_tex] == [f"{a['given']} {a['family']}" for a in authors["authors"]], "J", "R24 authors in the pipeline's order")
ok("\\author*[1]{\\fnm{Adam} \\sur{Park}}\\email{apark1@imsa.edu}" in tex, "J", "R24 and R28 corresponding author marked with an e-mail address")
for a in authors["authors"]:
    ok((a["orcid"] or "none") in decl, "J", f"R24 ORCID of {a['given']} {a['family']} listed")
for inst in ("Illinois Mathematics and Science Academy", "Princeton University", "University of Michigan", "Cornell Tech"):
    ok(re.search(re.escape(inst) + r"\}, \\orgaddress\{\\city\{[^}]+\}, \\state\{[^}]+\}, \\country\{USA\}\}", tex), "J", f"R28 affiliation {inst} with city, state and country")
ok("\\url{https://github.com/Guldek1987/bayesian-milling-monitoring-hbaf}" in tex and "hbaf" not in bibt.lower(), "J", "R29 U3 mentioned in the text with its URL and kept out of the reference list (Q4)")
ok("\\endnote" not in tex, "J", "R30 no endnote")
for m in re.finditer(r"\\footnotetext(\[[^\]]*\])?\{((?:[^{}]|\{[^{}]*\})*)\}", tex):
    ok(len(re.sub(r"\\cite\{[^}]*\}", "", m.group(2)).split()) > 3, "J", "R30 every footnote holds text besides any citation")

# ------------------------------------------------------------------ F. statements in the text that describe values
T = lambda n: ALL[n]["value"]
CI = lambda n: (ALL[n]["lo"], ALL[n]["hi"])
THR = T("thr_auc_drop")
ok(T("d9_paths_gt1pct_imi_vm20i_s0_l1") == 1.0 and T("e1_imi_vm20i_l1_any_clip") == 1.0, "F", "'every label 1 path clips Sound0'")
ok(T("e1_imi_vm20i_bin4_l1_share") == 1.0, "F", "'all of which are label 1' (paths above e1_bin_edge_high)")
ok(T("pop_imi_vm20i_b_l1") == 0 and T("pop_imi_vm20i_c1_l1") == 0, "F", "'families B and C1 hold none'")
ok(T("facts_l1_d_f1") == T("pop_imi_vm20i_d_l1") + T("pop_imi_vm20i_f1_l1"), "F", "'families D and F1 hold ... between them' equals the sum of their Phase 3 counts")
ok(T("vm20i_max_families_per_run") == 1, "F", "'each run used a single tool family' (imi_vm20i)")
vmx = QC.load_paths("imi_vmx30ui")
ok(int(vmx.groupby("run").family.nunique().max()) == 1 and int(vmx.groupby("run").Workpiece.nunique().max()) == 1
   and int(paths.groupby("run").Workpiece.nunique().max()) == 1, "F", "'each run used one tool family and one workpiece material' (Table 1 note, both machines)")
ok(T("pop_agree_excluded_slow_l1") == 0, "F", "'which removes ... paths, all of them label 0' (paths below the comb floor)")
ok(T("pop_h3_primary_lo_families") > 1 and T("pop_h3_primary_hi_families") > 1, "F", "'each speed class holds label 1 paths of more than one family'")
ok(T("holm_n_tested") == T("tally_total") and T("h1_nonfinite") == 0 and T("h2_nonfinite") == 0 and T("h3_nonfinite") == 0, "F", "'all predictions passed their testability conditions, and no resample left a statistic undefined'")
ok(0 < T("h1_drop") < THR and T("h1_class") == "FAILED_MAGNITUDE", "F", "'H1 failed on magnitude; the fall lies on the predicted side of zero and below the threshold'")
ok(CI("h1_drop_ci")[1] < THR, "F", "'an interval that lies wholly below the threshold' (H1, Discussion)")
ok(0 < T("h2_drop") < THR and CI("h2_drop_ci")[1] >= THR and CI("h2_drop_ci")[0] > 0 and T("h2_class") == "FAILED_MAGNITUDE", "F", "'H2 exceeds zero under the bootstrap, stays below the threshold, which its interval includes'")
ok(close(T("h2_p_holm"), 3 / (B + 1), 1e-12), "F", "'the smallest Holm-adjusted p-value that boot_b resamples allow'")
ok(T("a1_h2_flips") == len(T("facts_a1_h2_flip_runs").split(" or ")) and T("a1_h2_max") >= THR, "F", "'leaving out run X or run Y alone would make the rule hold'")
ok(T("a1_h2_max") - T("a1_h2_min") >= CI("h2_drop_ci")[1] - CI("h2_drop_ci")[0], "F", "'a range as wide as the bootstrap interval' (H2 leave-one-run-out)")
ok(T("ph_jack_z0_h2_drop") < 1.96 and T("ph_jack_zthr_h2_drop") < 1.96, "F", "'resolved neither against zero nor against the threshold' (H2 on the jackknife scale, fewer than 1.96 standard errors from each)")
ok(T("ph_jack_ratio_h1_drop") > 1 and T("ph_jack_ratio_h2_drop") > 1, "F", "'the intervals of H1 and H2 are too narrow' (jackknife standard error above the bootstrap's)")
ok(0.8 < T("ph_jack_ratio_h3_kappa") < 1.25, "F", "'for kappa the jackknife standard error is close to the bootstrap's' (ratio between 0.8 and 1.25)")
ok(T("ph_jack_top3_share_h1_drop") > 0.5 and T("ph_jack_top3_share_h2_drop") > 0.5, "F", "'runs ... dominate it' (more than half of the jackknife sum of squares from three runs)")
ok(T("a1_h1_flips") == 0 and T("a1_h3_flips") == 0 and T("a1_h3_margin_min") > 0, "F", "no single run flips H1 or H3")
ok(T("h3_class") == "CONFIRMED" and T("h3_contradicting") == "none" and T("rob_h3_below_list") == "none", "F", "'H3 was confirmed' and 'no robustness variant contradicted this result'")
ok(T("h1_contradicting").startswith("not evaluated") and T("h2_contradicting").startswith("not evaluated"), "F", "variants of the failed predictions recorded as not evaluated (review item R7)")
for c in ("lo", "hi"):
    ok(T(f"h3_tab_{c}_both") > T(f"h3_tab_{c}_acc0_only") + T(f"h3_tab_{c}_s2_only"), "F", f"'paths with both lines outnumber those with one line' ({c})")
ok(T("a4_kappa") >= T("a4_thr") and T("a6_kappa") >= T("a6_thr"), "F", "H3 checks A4 and A6 stay above their thresholds")
ok(T("ph_h3_run_thr") < T("h3_kappa") and ALL["ph_h3_run_excess_ci"]["lo"] > 0, "F", "'the post hoc threshold within runs lies below the observed kappa, and kappa exceeded that null'")
ok(T("h1_v3_drop") > T("h1_drop") and T("h2_v3_drop") > T("h2_drop"), "F", "'the larger losses of the random forest' (H1 and H2)")
ok(T("facts_v6c_changed_acc0") == T("facts_v7_changed_acc0") and T("facts_v6c_l1_changed") == 0 and T("facts_v7_l1_changed") == 0
   and T("facts_v6c_changed_s2") == 0 and T("facts_v7_changed_s2") == 0, "F", "'V6c and V7 each changed the Acc0 indicator of the same number of paths, none of them label 1, and no Sound2 indicator'")
below_h1 = [v.upper() for v in ("v1", "v2", "v3", "v5") if T(f"rob_h1_{v}") == "below"]
ok(T("rob_h1_below_list") == (", ".join(below_h1[:-1]) + " and " + below_h1[-1] if len(below_h1) > 1 else (below_h1[0] if below_h1 else "none")) and T("rob_h1_v3") == "reaches",
   "F", "'for H1 the intervals of the listed variants lie wholly below the threshold, while V3 reaches it'")
ok(T("rob_h2_below_list") == "none" and T("h2_v3_drop") > THR, "F", "'for H2 every variant's interval reaches the threshold, and the random forest's estimate exceeds it'")
for v in ("v1", "v2", "v3", "v5"):
    for h in ("h1", "h2"):
        ok((T(f"rob_{h}_{v}") == "below") == (CI(f"{h}_{v}_drop_ci")[1] < THR), "F", f"Table 4 mark of {h.upper()} {v.upper()} matches its interval")
for v in ("v1", "v2", "v6a", "v6b", "v6c", "v7", "v9"):
    ok((T(f"rob_h3_{v}") == "below") == (CI(f"h3_{v}_kappa_ci")[1] < T(f"h3_{v}_thr")), "F", f"Table 4 mark of H3 {v.upper()} matches its interval")
ok(V4["e4_metanf_auc_family_ci"]["lo"] <= T("auc_chance") <= V4["e4_metanf_auc_family_ci"]["hi"], "F", "'an interval consistent with chance ranking' (design without family, families held out)")
ok(T("e10_h2_family_across") < T("e10_h2_run_across") and T("e10_h2_family_within") >= T("e10_h2_run_within") - 0.01, "F", "'the MTConnect across-family AUC fell while the within-family AUC held'")
ok(T("e11_rpm_hi_share_a") > T("e11_rpm_hi_share_e"), "F", "'family A's label 1 paths also ran faster'")
ok(T("e6_l0_both_fam_a") > T("e6_l0_both_n") / 2, "F", "'the label 0 paths with both lines cluster in family A'")
ok(T("e9_share_match") > V4["e9_share_baseline_ci"]["hi"] and T("e9_share_match") > 0.5, "F", "'the lines usually share a frequency' (above the baseline interval and above one half)")
ok(T("vm20i_a_aa6061_n") / (T("pop_imi_vm20i_a_l0") + T("pop_imi_vm20i_a_l1")) > 0.9, "F", "'family A was cut almost only in AA 6061'")
ok(T("e1_imi_vmx30ui_s0_l01_gt1pct") < T("e1_imi_vm20i_s0_l01_gt1pct"), "F", "'the Sound0 microphone of imi_vmx30ui clips on fewer paths' (labels 0 and 1)")
ok(T("e1_imi_vmx30ui_s0_l1_gt1pct") < T("d9_paths_gt1pct_imi_vm20i_s0_l1") and T("ph_clip_auc_raw_imi_vmx30ui") < T("ph_clip_auc_raw_imi_vm20i")
   and ALL["ph_clip_auc_raw_imi_vmx30ui_ci"]["lo"] > T("auc_chance"), "F", "'on imi_vmx30ui the relation is weaker but present'")
ok(T("e7_highrate_auc_material") >= T("h1_auc_run") - 0.01, "F", "'the model also ranked well when whole materials were left out'")
ok(T("e2_clip_auc_run") >= T("h1_auc_run") - 0.01, "F", "'the clipping score is close to the full high-rate set'")
ok(T("ph_freq_acc0_l1_rho_rpm") > 0 and T("ph_freq_s2_l1_rho_rpm") > 0, "F", "'the line frequency rose with the commanded spindle speed' (both sensors)")
khz = lambda n: round(T(n) / 1000, 1)
ok(khz("ph_freq_acc0_l0_q25") == khz("ph_freq_acc0_l1_q25") and khz("ph_freq_acc0_l0_q75") == khz("ph_freq_acc0_l1_q75"), "F", "'the lines of label 0 and label 1 paths fall in the same frequency range' (Acc0 quartiles equal at the printed precision)")
ok(len({khz(f"ph_fig2_l{L}_{c}_hz") for L in (0, 1) for c in ("acc0", "s2")}) == 1, "F", "Fig. 2 caption: 'in both paths the dominant line of both sensors lies near the same frequency'")
ok(T("e6_l0_both_share") < 0.05, "F", "Fig. 2 caption: 'as only a few percent of label 0 paths do'")
ok(T("e5_label0_share_max") == max(T(f"e5_{ch}_{st}_l0") for ch in ("acc0", "s2") for st in ("base", "comb1", "comb3", "rel", "band5k")), "F", "'in label 0 neither exceeded' the largest Acc0 or Sound2 share")
ok(T("e5_acc1_comb1_l0") > T("e5_label0_share_max"), "F", "'while Acc1 reached' more with the narrow comb")
# Table 1: counts by family and material add up to the machine totals, and every combination with paths has a row
for mt, mach, nr, n0, n1 in (("vm20i", "imi_vm20i", "h1_n_runs", "n_paths_imi_vm20i_l0", "n_paths_imi_vm20i_l1"), ("vmx30ui", "imi_vmx30ui", "e8_n_runs", "n_paths_imi_vmx30ui_l0", "n_paths_imi_vmx30ui_l1")):
    rows_ = sorted({k[len(f"tab1_{mt}_"):-len("_runs")] for k in ALL if k.startswith(f"tab1_{mt}_") and k.endswith("_runs")})
    ok(sum(T(f"tab1_{mt}_{r}_runs") for r in rows_) == T(nr) and sum(T(f"tab1_{mt}_{r}_l0") for r in rows_) == T(n0) and sum(T(f"tab1_{mt}_{r}_l1") for r in rows_) == T(n1),
       "F", f"Table 1 rows of {mach} add up to its totals")
    ok(all(f"{{{{tab1_{mt}_{r}_runs|int}}}}" in tpl for r in rows_), "F", f"every family and material combination of {mach} has its Table 1 row")
ok(tpl.count("}{not used") == len({(f, m) for f in ("a", "b", "c1", "c2", "d", "e", "f1", "f2") for m in ("aa6061", "aisi1018", "aisi4140")
                                    if (f"tab1_vm20i_{f}_{m}_runs" in ALL) != (f"tab1_vmx30ui_{f}_{m}_runs" in ALL)}), "F", "Table 1 marks 'not used' exactly where a combination exists on one machine only")
ok(all(f"{{{{auc_row_{t}|text}}}}" in tpl for t in ("highrate", "mtc", "nos0", "clip", "metanf", "meta")), "F", "Table 3 prints the row names that Fig. 4 uses (shared values)")
ok("not computed" not in tex, "F", "no caption mentions cells that do not exist")
for i in (1, 3):
    rows_esm = [l for l in open(f"{P4}/esm/ESM_{i}.csv").read().split("\n")[1:] if l and not l.startswith("#")]
    ok(len(rows_esm) - 1 == T("h1_n_paths"), "F", f"ESM_{i} holds the label 0 and label 1 paths it is described as holding")
# exploratory, post hoc and seen values carry their label in the abstract, Discussion and Conclusions (proposal C33, review item R5)
PREFIX_X = ("e1_", "e2_", "e3_", "e4_", "e5_", "e6_", "e7_", "e8_", "e9_", "e10_", "e11_", "ph_", "s1_", "s3_", "d3_", "d4_", "d5_", "d9_")
LABELS = ("exploratory", "post hoc", "seen before")
ctx = {"abstract": tpl[tpl.index("%%ABSTRACT-START"):tpl.index("%%ABSTRACT-END")],
       "discussion": tpl[tpl.index("\\section{Discussion}"):tpl.index("\\backmatter")]}
for where, block in ctx.items():
    for para in [q for q in re.split(r"\n\s*\n", block) if "{{" in q]:
        xs = [n for n in re.findall(r"\{\{([a-z0-9_]+)\|", para) if n.startswith(PREFIX_X) and n != "d9_threshold"]
        if xs:
            ok(any(lb in para.lower() for lb in LABELS), "F", f"{where}: a paragraph that cites exploratory, post hoc or seen values labels them ({xs[0]})")

# ------------------------------------------------------------------ K. figure files
insp = json.load(open(f"{P4}/inputs/figure_inspection.json"))
for i in range(1, 6):
    eps = open(f"{P4}/figures/Fig{i}.eps", "rb").read().decode("latin-1")
    ok(eps.startswith("%!PS-Adobe-3.0 EPSF-3.0"), "K", f"R13 Fig{i} is EPS")
    m = re.search(r"%%HiResBoundingBox: ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+)", eps) or re.search(r"%%BoundingBox: ([\d.]+) ([\d.]+) ([\d.]+) ([\d.]+)", eps)
    w_mm, h_mm = (float(m.group(3)) - float(m.group(1))) * 25.4 / 72, (float(m.group(4)) - float(m.group(2))) * 25.4 / 72
    ok(min(abs(w_mm - 84), abs(w_mm - 174)) < 0.6 and h_mm <= 234, "K", f"R11 Fig{i} is {w_mm:.1f} by {h_mm:.1f} mm")
    ok(abs(w_mm - insp["figures"][f"Fig{i}"]["size_mm"][0]) < 0.6 and abs(h_mm - insp["figures"][f"Fig{i}"]["size_mm"][1]) < 0.6, "K", f"Fig{i} has its budgeted size")
    sizes = {float(x) for x in re.findall(r"/[A-Za-z-]+ ([\d.]+) selectfont", eps)}
    ok(sizes == {float(g23("fig_font_pt"))}, "K", f"R12 Fig{i} lettering is {sizes} pt")
    ok("/FontType 42" in eps and "LiberationSans" in eps, "K", f"R12 Fig{i} embeds its font (Liberation Sans, Type 42)")
    lw = [float(x) for x in re.findall(r"([\d.]+) setlinewidth", eps)]
    ok(lw and min(x for x in lw if x > 0) >= g23("fig_line_pt") - 1e-9, "K", f"R12 Fig{i} thinnest line {min(x for x in lw if x > 0)} pt")
    ok("setcmykcolor" not in eps, "K", f"R13 Fig{i} uses RGB colour")
    ok(f"Fig{i}" in insp["figures"] and insp["figures"][f"Fig{i}"]["fixed"], "K", f"Fig{i} inspection recorded with its fixes")

# ------------------------------------------------------------------ L. Online Resources, deviations, audit, review, open items
for i in (1, 3, 4):
    txt = open(f"{P4}/esm/ESM_{i}.csv").read()
    head = [l for l in txt.split("\n") if l.startswith("# ")]
    ok(any(l.startswith("# Article: ") and title in l for l in head) and any("Journal: The International Journal of Advanced Manufacturing Technology" in l for l in head)
       and any("apark1@imsa.edu" in l and "Illinois Mathematics and Science Academy" in l for l in head), "L", f"R21 ESM_{i}.csv carries title, journal, authors and the corresponding author's affiliation and e-mail")
    ok(any(f"Online Resource {i} (ESM_{i}.csv)" in l for l in head), "L", f"R21 ESM_{i}.csv names itself as Online Resource {i}")
e4 = pd.read_csv(f"{P4}/esm/ESM_4.csv", comment="#")
ok(set(e4.analysis) == {"E1", "E5"} and all(n in ALL and close(ALL[n]["value"], v, 1e-4) for n, v in zip(e4.value_name, e4.share)), "L", f"ESM_4 holds the E1 and E5 tables and every share equals its value ({len(e4)} rows)")
ok(len(e4[e4.analysis == "E5"]) == 4 * 5 * 2, "L", "ESM_4 gives E5 for four channels, five indicator settings and both labels")
ZIP = f"{P4}/esm/ESM_2.zip"
ok(os.path.exists(f"{P4}/code/r12_package.py"), "L", "R21 ESM_2.zip has its builder (r12_package.py)")
if PREPACK:
    pass
elif os.path.exists(ZIP):
    import zipfile
    with zipfile.ZipFile(ZIP) as z:
        zn = set(z.namelist())
        ok(not any("/" in n for n in zn), "L", f"ESM_2.zip has no subfolders ({len(zn)} files, all at the top level)")
        readme = z.read("README.txt").decode()
        ok(f"Article: {title}" in readme and "Journal: The International Journal of Advanced Manufacturing Technology" in readme and "apark1@imsa.edu" in readme
           and "Illinois Mathematics and Science Academy" in readme, "L", "R21 ESM_2.zip README carries the title-page details")
        for arc, name in (("plan.json", "freeze_sha256_plan"), ("q_common.py", "freeze_sha256_q_common"), ("q_confirm.py", "freeze_sha256_q_confirm"), ("q_rules.py", "freeze_sha256_q_rules")):
            ok(arc in zn and hashlib.sha256(z.read(arc)).hexdigest() == V3[name]["value"] and V3[name]["value"] in readme, "L", f"ESM_2.zip holds {arc} with its frozen digest, listed in the README")
        zc = json.loads(z.read("check4_result.json"))
        ok(zc["status"] == "PASS" and zc.get("prepackage") is True, "L", "ESM_2.zip holds the passing pre-packaging check result")
        man = json.load(open(f"{P4}/outputs/esm2_manifest.json"))
        ok(hashlib.sha256(open(ZIP, "rb").read()).hexdigest() == man["sha256"], "L", "ESM_2.zip is the archive recorded in its manifest")
        stale = [a for a, d in man["manifest"].items() if a in zn and a not in ("check4_result.json", "values4.json")
                 and hashlib.sha256(z.read(a)).hexdigest() != d]
        ok(not stale, "L", f"ESM_2.zip matches its manifest ({len(man['manifest'])} files)")
        ok("r11_review.py" in zn and "deviations4.json" in zn and "values4.json" in zn, "L", "ESM_2.zip holds the post hoc code, the deviations log and the values")
        ok(not any("authors.json" in n or "review4" in n or "self_audit" in n or "open_items" in n for n in zn), "L", "ESM_2.zip leaves out contact details and internal review records")
        needle = ("@" + "gm" + "ail." + "com").encode()
        ok(not any(needle in z.read(n) for n in zn), "L", "ESM_2.zip holds no personal e-mail address of the user")
else:
    ok(False, "L", "ESM_2.zip exists (run r12_package.py after a passing check)")
dev = json.load(open(f"{P4}/inputs/deviations4.json"))
ok(all({"id", "kind", "what", "effect", "when"} <= set(d) for d in dev["items"]) and len({d["id"] for d in dev["items"]}) == len(dev["items"]), "L", f"deviations log complete ({len(dev['items'])} entries)")
dids = {d["id"]: d for d in dev["items"]}
for did, prefix in (("DV13", "ph_clip_auc_raw_"), ("DV14", "ph_jack_se_"), ("DV15", "e1_imi_vmx30ui_s0_l1"), ("DV16", "ph_h3_run_"), ("DV17", "ph_freq_")):
    ok(did in dids and any(k.startswith(prefix) for k in V4) and "review" in dids[did]["when"], "L", f"{did} logged, with its values registered after the review ({prefix}*)")
ok("DV18" in dids and "pre-specified" in dids["DV18"]["what"], "L", "DV18 records the change from pre-registered to pre-specified")
sa = json.load(open(f"{P4}/inputs/self_audit4.json"))
ok(all(it.get("disposition") in ("addressed", "accepted as a limitation", "deferred") for it in sa["items"]), "L", f"self_audit4.json: every item has a disposition ({len(sa['items'])} items)")
ok(sa.get("post_review") and all(u.get("item") and u.get("update") for u in sa["post_review"]), "L", "self_audit4.json records what the review changed, without rewriting the original items")
rep_md = open(f"{P4}/inputs/review4_round1_report.md").read()
ok("model output, not an instruction from Adam" in rep_md and "Edits:" in rep_md and all(f"R{i} (" in rep_md for i in range(1, 15)), "L", "the review report is kept verbatim with a header that names its source and edits")
rv = json.load(open(f"{P4}/inputs/review4_dispositions.json"))
ok([it["id"] for it in rv["items"]] == [f"RV{i}" for i in range(1, 15)] and [it["review_item"] for it in rv["items"]] == [f"R{i}" for i in range(1, 15)], "L", "every review item has a disposition record (RV1 to RV14)")
ok(all(it.get("disposition") in ("addressed", "addressed differently", "accepted as a limitation", "deferred", "rejected") and it.get("verified") and it.get("action") for it in rv["items"]), "L", "every review disposition says how the item was verified and what was done")
ok(rv["second_round"] is False and "root-cause" in rv["second_round_reason"], "L", "no second review round, with the reason (C6)")
rc = open(f"{P4}/inputs/review4_root_cause.md").read()
ok(all(f"| C{n} |" in rc for n in range(33, 42)) and "pipeline/prompt_changes.md is not changed" in rc, "L", "root-cause note proposes C33 to C41 as proposals only")
covered = set(re.findall(r"RV\d+", rc))
ok(covered >= {f"RV{i}" for i in range(1, 15)}, "L", "every review item is traced to a root cause")
OPEN = json.load(open(f"{P4}/inputs/open_items4.json"))
ok({"funding", "llm_paragraph", "post_hoc"} <= {it["id"] for it in OPEN["items"]}, "L", "open items carry the funding statement, the language-model paragraph and the post hoc analyses for the authors")
for f in glob.glob(f"{P4}/code/*.py") + glob.glob(f"{P4}/inputs/*.json") + glob.glob(f"{P4}/inputs/*.md") + glob.glob(f"{P4}/tex/src/*.tex") + glob.glob(f"{P4}/esm/*.csv"):
    s_ = open(f, encoding="utf-8", errors="replace").read()
    ok(chr(0x2014) not in s_ and chr(0x2013) not in s_, "L", f"no dash character in {os.path.basename(f)}")

# ------------------------------------------------------------------ M. analyses added after the review, recomputed
lab1m = paths[paths.y == 1].reset_index(drop=True)
iam, ibm = arr["h3_acc0"].astype(int), arr["h3_s2"].astype(int)
ok((arr["h3_run_id"] == lab1m.run_id.values.astype(str)).all(), "M", "the saved H3 indicators follow the label 1 paths in order")
knr, kmr = QC.kappa_null_w(iam, ibm, lab1m.run_id.values), QC.kappa_max_w(iam, ibm, lab1m.run_id.values)
ok(close(knr, T("ph_h3_run_kappa_null"), 1e-12) and close(kmr, T("ph_h3_run_kappa_max"), 1e-12), "M", "DV16 null and maximum within runs recomputed")
ok(close(QC.kappa_threshold(knr, kmr, g23("thr_kappa_frac")), T("ph_h3_run_thr"), 1e-12), "M", "DV16 threshold follows the registered rule")
Lr = pd.read_csv(f"{P4}/outputs/a1_leave_one_run_out.csv")
for col, name in (("h1", "h1_drop"), ("h2", "h2_drop"), ("h3", "h3_kappa")):
    th = Lr[col].values.astype(float); n_ = len(th)
    ok(close(np.sqrt((n_ - 1) / n_ * ((th - th.mean()) ** 2).sum()), T(f"ph_jack_se_{name}"), 1e-12), "M", f"DV14 jackknife standard error of {name} recomputed from the A1 refits")
for mach in ("imi_vm20i", "imi_vmx30ui"):
    dm = QC.load_paths(mach).dropna(subset=["s0_fullscale"])
    ok(close(QC.auc_w(dm.y.values, dm.s0_fullscale.values.astype(float)), T(f"ph_clip_auc_raw_{mach}"), 1e-12), "M", f"DV13 AUC of the full-scale share recomputed on {mach}")
lf = pd.read_csv(f"{P4}/outputs/line_frequencies.csv")
A3c = np.load(f"{P4}/outputs/r03_arrays.npz")
for ch in ("acc0", "s2"):
    on = A3c[f"{ch}_base"] == 1
    ok((lf[f"{ch}_line_hz"].notna().values == on).all(), "M", f"DV17 a {ch} line frequency exists exactly where the indicator is on")
    for lab in (0, 1):
        q = np.quantile(lf[f"{ch}_line_hz"].values[on & (y == lab)], [0.25, 0.5, 0.75])
        ok(close(q[0], T(f"ph_freq_{ch}_l{lab}_q25"), 1e-9) and close(q[1], T(f"ph_freq_{ch}_l{lab}_median"), 1e-9) and close(q[2], T(f"ph_freq_{ch}_l{lab}_q75"), 1e-9), "M", f"DV17 quartiles of the {ch} line frequency, label {lab}, recomputed")

# ------------------------------------------------------------------ result
failed = [(g, m) for c, g, m in RESULTS if not c]
now = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
res = {"status": "PASS" if not failed else "FAIL", "assertions": len(RESULTS), "failed": failed, "time_utc": now, "prepackage": PREPACK,
       "groups": {gname: sum(1 for c, g2, _ in RESULTS if g2 == gname) for gname in sorted({g2 for _, g2, _ in RESULTS})},
       "lint_warnings": len(warnings), "lint_stats": stats, "pages": pages, "words_main": n_words, "words_abstract": n_abs,
       "tally": {k: v for k, v in counts.items()}, "classes": {k: v[0] for k, v in classes.items()}}
json.dump(res, open(f"{P4}/outputs/check4_result.json", "w"), indent=1)
res["check_line"] = f"{res['status']}, {len(RESULTS):,} assertions run by r10_check.py at {now}" + (" (pre-packaging run)" if PREPACK else "")
json.dump(res, open(f"{P4}/outputs/check4_result.json", "w"), indent=1)
print(res["check_line"])
for g2, m in failed[:40]:
    print("FAILED", g2, m)
