main
Drew Owens 0.11.4 source 8889116 1d ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354
//! `swx_selftest` (0.10.0) — FIRST-CLASS SUPPORT WITHOUT THE MACHINE.
//!
//! Our first community user (Michael, issues #1 and #2) runs SOLIDWORKS 2021;
//! the bridge has only ever run against 3DEXPERIENCE SOLIDWORKS 2026. This verb
//! is how he — or anyone on a release we have not seen — can send back ONE JSON
//! that says, verb by verb, what works on their install.
//!
//! * `level:"read"` (the default) changes NOTHING: status, readiness, the
//!   document list, preferences, and the read verbs against whatever document
//!   is active.
//! * `level:"write"` ALSO runs the GEOMETRIC PROOF (0.10.2): 31 checks, each
//!   building its own scratch part (never a user document), MEASURED with
//!   swx_inspect_bodies / swx_mass_properties against geometry computed HERE
//!   from closed-form formulas — the same battery as tools/dsl-live-check.py,
//!   each row carrying expected vs measured — then closes what it made. Then,
//!   on one part made by swx_new_document: issue #2's ownership, a dimension, a
//!   STEP export, a save, a close, and the user's active document put back.
//!
//! Every sub-call goes through the normal dispatcher, gates included: the
//! self-test is held to exactly the rules a caller is.

use serde_json::{json, Value};
use std::time::Instant;

use crate::verbs::Ctx;

struct Run {
    results: Vec<Value>,
}

/// Michael (2026-10-15) ran it with the CLI's default budget, which cut it off
/// and left the first scratch part open.
pub const TIMEOUT_HINT: &str = "call this with responseMode:\"stream\" or \"job\" (it runs 5-8 minutes; the CLI's default 120 s budget cuts it off and leaves the first scratch part open — the next swx_selftest closes it, and swx_status lists it)";

// ── ORPHANS (0.10.10) ───────────────────────────────────────────────────────
// MEASURED 2026-10-15 (Michael, SOLIDWORKS 2021): the CLI's default 120 s budget
// cut a 5-8 minute self-test off, and its scratch part was left open and dirty.
// Every document the self-test creates is RECORDED here (data root,
// `selftest-docs.json`) and struck off when it is closed; whatever is still
// recorded when the next self-test starts is that run's orphan, and is closed
// (the bridge's own, never saved by anyone). swx_status REPORTS them only: a
// status call may arrive while a self-test is running, and must not close the
// part it is building.

static RUNNING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);

fn orphan_file() -> std::path::PathBuf {
    crate::scratch::root().join("selftest-docs.json")
}

/// The documents a self-test made and has not closed (yet).
pub fn recorded_docs() -> Vec<Value> {
    std::fs::read_to_string(orphan_file()).ok().and_then(|t| serde_json::from_str::<Vec<Value>>(&t).ok()).unwrap_or_default()
}

fn write_docs(v: &[Value]) {
    let _ = std::fs::write(orphan_file(), serde_json::to_string(v).unwrap_or_else(|_| "[]".into()));
}

/// Record a document the self-test made: `key` is its path, else its title.
fn record_doc(key: &str) {
    if key.is_empty() {
        return;
    }
    let mut v = recorded_docs();
    if !v.iter().any(|d| d["key"] == key) {
        v.push(json!({"key": key, "at": crate::scratch::stamp_now()}));
        write_docs(&v);
    }
}

fn forget_doc(key: &str) {
    let mut v = recorded_docs();
    v.retain(|d| d["key"] != key);
    write_docs(&v);
}

fn close_args(key: &str) -> Value {
    if key.contains('\\') || key.contains('/') {
        json!({"path": key, "save": false})
    } else {
        json!({"targetDocument": key, "save": false})
    }
}

/// Close what a previous, cut-off self-test left open. Returns what was found.
fn sweep_orphans(ctx: &Ctx) -> Vec<Value> {
    let mut out = Vec::new();
    for d in recorded_docs() {
        let key = d["key"].as_str().unwrap_or("").to_string();
        let r = crate::verbs::dispatch(ctx, "swx_close_doc", &close_args(&key));
        let closed = r.get("success").and_then(|v| v.as_bool()) != Some(false);
        let gone = !closed && r.get("errorCode").and_then(|c| c.as_str()) == Some("document_not_open");
        if closed || gone {
            forget_doc(&key);
        }
        out.push(json!({"document": key, "recordedAt": d["at"], "closed": closed, "alreadyGone": gone,
                        "error": if closed || gone { Value::Null } else { r.get("error").cloned().unwrap_or(Value::Null) }}));
    }
    out
}

/// For swx_status: the self-test's orphans, reported (never closed there).
pub fn orphan_report() -> Value {
    let docs = recorded_docs();
    if docs.is_empty() || RUNNING.load(std::sync::atomic::Ordering::SeqCst) {
        return Value::Null;
    }
    json!({"documents": docs, "why": "a swx_selftest run that ended early (cut off by a client timeout) made these and did not close them; the next swx_selftest closes them first, or swx_reset_workspace does"})
}

/// Clears the RUNNING flag however the run ends.
struct RunGuard;
impl Drop for RunGuard {
    fn drop(&mut self) {
        RUNNING.store(false, std::sync::atomic::Ordering::SeqCst);
    }
}

impl Run {
    /// One verb, recorded. Returns the reply.
    fn call(&mut self, ctx: &Ctx, name: &str, verb: &str, args: Value) -> Value {
        let t = Instant::now();
        let r = crate::verbs::dispatch(ctx, verb, &args);
        let pass = r.get("success").and_then(|v| v.as_bool()) != Some(false);
        let mut row = json!({
            "name": name,
            "verb": verb,
            "pass": pass,
            "ms": t.elapsed().as_millis() as u64,
        });
        if !pass {
            row["errorCode"] = r.get("errorCode").cloned().unwrap_or(Value::Null);
            row["error"] = json!(r.get("error").and_then(|e| e.as_str()).unwrap_or("").chars().take(400).collect::<String>());
            row["hostError"] = r.get("hostError").cloned().unwrap_or(Value::Null);
        }
        self.results.push(row);
        r
    }

    fn skip(&mut self, name: &str, verb: &str, why: &str) {
        self.results.push(json!({"name": name, "verb": verb, "pass": Value::Null, "skipped": why}));
    }

    /// Attach a small detail to the last row.
    fn note(&mut self, detail: Value) {
        if let Some(last) = self.results.last_mut() {
            last["detail"] = detail;
        }
    }
}

pub fn selftest(ctx: &Ctx, args: &Value) -> Value {
    let level = args.get("level").and_then(|l| l.as_str()).unwrap_or("read");
    if !matches!(level, "read" | "write" | "cswa") {
        return crate::verbs::err_code(
            "bad_level",
            format!("level must be \"read\" (changes nothing), \"write\" (also builds, exports and closes a scratch part) or \"cswa\" (builds the CSWA benchmark parts and checks their mass), got {level:?}"),
            "Run level:\"read\" first; level:\"write\" only touches documents the self-test creates in the bridge's scratch folder.",
        );
    }
    let started = Instant::now();
    let mut run = Run { results: Vec::new() };
    if RUNNING.swap(true, std::sync::atomic::Ordering::SeqCst) {
        return crate::verbs::err_code(
            "selftest_running",
            "a swx_selftest is already running in this bridge",
            "Wait for it to finish (it can take 5-8 minutes at level write) — two runs at once would each close the other's scratch parts.",
        );
    }
    let _running = RunGuard;

    // ── READ: changes nothing ───────────────────────────────────────────────
    let status = run.call(ctx, "status", "swx_status", json!({}));
    let sw = status.get("solidworks").cloned().unwrap_or(Value::Null);
    run.note(json!({
        "edition": sw.get("edition"),
        "launchRoute": sw.get("launchRoute"),
        "versionSupport": sw.get("versionSupport"),
        "registryYears": sw.get("registryYears"),
        "classicYears": sw.get("classicYears"),
        "registryVersions": sw.get("registryVersions"),
    }));
    let readiness = run.call(ctx, "readiness", "swx_readiness", json!({}));
    let ready = readiness.get("ready").and_then(|v| v.as_bool()) == Some(true)
        || readiness.pointer("/solidworks/ready").and_then(|v| v.as_bool()) == Some(true);
    // IS SOLIDWORKS THERE TO TEST? MEASURED 2026-10-13 (hosted 0.10.0, SOLIDWORKS
    // closed): swx_docs and swx_get_preferences were reported as FAILURES with
    // "SOLIDWORKS is not running" — not a verb failing on this release, and a
    // user who ran the self-test before starting SOLIDWORKS would have sent us
    // false failures. So: every check that needs SOLIDWORKS is SKIPPED (why
    // said) when COM is not attached; status and readiness still run. A COM
    // host busy with a long verb is "cannot tell", and the checks run.
    let com = status.get("com").cloned().unwrap_or(Value::Null);
    let running = com.get("running").and_then(|v| v.as_bool()).or_else(|| com.get("attached").and_then(|v| v.as_bool()))
        .unwrap_or(com.get("busy").and_then(|v| v.as_bool()) == Some(true));
    if !running {
        let process = sw.get("running").and_then(|v| v.as_bool()) == Some(true);
        let why = if process {
            "SOLIDWORKS is running but this bridge is not attached to it yet (still starting, or waiting on a sign-in)"
        } else {
            "SOLIDWORKS is not running"
        };
        let ran = run.results.len();
        let start_hint = format!(
            "Start SOLIDWORKS (swx_start, or by hand) and run this again — with it closed only {ran} checks can run."
        );
        if level != "read" {
            // Refused UP FRONT: nothing was created or changed.
            let mut out = crate::verbs::err_code(
                "solidworks_not_running",
                format!("swx_selftest level:\"{level}\" needs SOLIDWORKS: {why}. Nothing was created or changed."),
                start_hint,
            );
            if let Value::Object(m) = &mut out {
                m.insert("results".into(), Value::Array(run.results));
                m.insert("solidworksRunning".into(), json!(false));
            }
            return out;
        }
        for (name, verb) in [
            ("document list", "swx_docs"),
            ("preferences", "swx_get_preferences"),
            ("the active document's read verbs", "swx_design_info"),
        ] {
            run.skip(name, verb, why);
        }
        return finish(level, &sw, ready, false, started, run, Some(start_hint));
    }
    // A CUT-OFF RUN'S PARTS, closed first (0.10.10).
    if level != "read" && !recorded_docs().is_empty() {
        let swept = sweep_orphans(ctx);
        run.results.push(json!({"name": "close what a cut-off self-test left open", "verb": "swx_close_doc",
                                "pass": swept.iter().all(|d| d["closed"] == true || d["alreadyGone"] == true), "detail": swept}));
    }
    let docs = run.call(ctx, "document list", "swx_docs", json!({}));
    let active = docs.pointer("/active/title").and_then(|t| t.as_str()).map(String::from);
    let active_type = docs.pointer("/active/type").and_then(|t| t.as_str()).map(String::from);
    run.call(ctx, "preferences", "swx_get_preferences", json!({}));
    match (&active, active_type.as_deref()) {
        (Some(t), kind) => {
            let a = json!({"targetDocument": t});
            run.call(ctx, "active document info", "swx_document_info", a.clone());
            run.call(ctx, "design info", "swx_design_info", a.clone());
            run.call(ctx, "parameters", "swx_get_parameters", a.clone());
            run.call(ctx, "custom properties", "swx_props_get", a.clone());
            if kind != Some("drawing") {
                run.call(ctx, "mass properties", "swx_mass_properties", a.clone());
            }
            match kind {
                Some("part") => {
                    run.call(ctx, "bodies", "swx_inspect_bodies", a.clone());
                }
                Some("assembly") => {
                    run.call(ctx, "bill of materials", "swx_bom", a.clone());
                }
                _ => {}
            }
        }
        (None, _) => run.skip("the active document's read verbs", "swx_design_info", "no document is open — open any part to include them"),
    }

    // ── CSWA: the benchmark parts, each built and weighed (0.11.0) ─────────
    if level == "cswa" {
        return cswa_level(ctx, args, &sw, ready, started, run, active);
    }

    // ── WRITE: the geometric proof, then the verbs on one scratch part ──────
    if level == "write" {
        // THE GEOMETRIC PROOF (0.10.2): every modelling op builds a scratch part
        // and the result is MEASURED against geometry computed here, from
        // closed-form formulas and the documented conventions — never against
        // the bridge's own idea of what it built. A row is a pass only if the
        // geometry matches. The same checks as tools/dsl-live-check.py.
        let only: Vec<String> = args
            .get("only")
            .and_then(|o| o.as_array())
            .map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
            .unwrap_or_default();
        // MEMORY, BETWEEN EVERY CHECK (0.10.5). Measured 2026-10-14: about a
        // hundred scratch parts built and closed in one bridge-started session
        // left SOLIDWORKS holding 10.6 GB with no documents open, and the
        // machine at 94 % committed. Before each check the run reads memory;
        // over the session budget or under pressure it RECYCLES a session the
        // bridge started with nothing open (swx_stop, swx_start, ready), and
        // otherwise STOPS — it never ends a session that is not its own.
        let memory_override = crate::memory::override_reason(args).ok().flatten();
        let idle_wait = crate::presence::wait_for_idle_secs(args);
        let mut memory_log = MemoryLog::default();
        let mut last_check: Option<&'static str> = None;
        // ONE VISIBLE DOCUMENT FOR THE BATTERY (0.10.7). MEASURED 2026-10-14:
        // every NEW part costs SOLIDWORKS ~85 MB and ~125 handles until it
        // restarts, while 12 builds in ONE reused part left it flat (3260 ->
        // 3246 MB). Every check that can runs in one part the bridge made,
        // cleared between checks (clear_part); a check that needs a fresh part
        // (a material, a colour, a feature named by its number) gets its own.
        // VISIBLE: a hidden part cannot be modelled (measured).
        let mut shared: Option<String> = None;
        for check in battery() {
            if !only.is_empty() && !only.iter().any(|o| o == check.id) {
                continue;
            }
            if let Err(stop) = presence_between(ctx, last_check, check.id, idle_wait, &mut shared) {
                memory_log.stopped_for = Some("userPresent");
                memory_log.stopped = Some(stop);
                break;
            }
            if let Err(stop) = memory_between(ctx, last_check, check.id, memory_override.as_deref(), &mut memory_log, &mut shared) {
                memory_log.stopped = Some(stop);
                break;
            }
            let reuse = reusable(&check);
            if reuse && shared.is_none() {
                shared = make_shared_document(ctx, memory_override.as_deref());
                if shared.is_some() {
                    memory_log.documents_created += 1;
                }
            }
            let target = if reuse { shared.clone() } else { None };
            let docs = geometry_check(ctx, &mut run, &check, memory_override.as_deref(), target.as_deref());
            if docs.fresh {
                memory_log.documents_created += 1;
            }
            let clear_failed = docs.clear_failed;
            let after = crate::memory::sample(crate::launchos::os().as_ref());
            if let Some(last) = run.results.last_mut() {
                last["swPrivateMbAfter"] = json!(after.sw_private_mb);
                last["reusedDocument"] = json!(target.is_some());
            }
            if clear_failed {
                // A shared part that could not be cleared is replaced, not reused.
                close_shared(ctx, &mut shared);
            }
            last_check = Some(check.id);
        }
        close_shared(ctx, &mut shared);
        if memory_log.stopped.is_none() {
            if let Err(stop) = presence_between(ctx, last_check, "the verbs on a scratch part", idle_wait, &mut shared) {
                memory_log.stopped_for = Some("userPresent");
                memory_log.stopped = Some(stop);
            }
        }
        if memory_log.stopped.is_none() {
            if let Err(stop) = memory_between(ctx, last_check, "the verbs on a scratch part", memory_override.as_deref(), &mut memory_log, &mut shared) {
                memory_log.stopped = Some(stop);
            }
        }
        if memory_log.stopped.is_some() {
            if let Some(prev) = &active {
                run.call(ctx, "re-activate the document that was active before", "swx_activate_document", json!({"name": prev}));
            }
            return finish_with(level, &sw, ready, true, started, run, None, memory_log.to_json(memory_override.as_deref()));
        }

        // THE VERBS on one scratch part made by swx_new_document.
        let mut left_open: Vec<Value> = Vec::new();
        let mut new_doc = json!({"type": "part"});
        memory_log.documents_created += 1;
        if let Some(r) = &memory_override {
            new_doc["ignoreMemoryPressure"] = json!(true);
            new_doc["reason"] = json!(r);
        }
        let made = run.call(ctx, "new scratch part", "swx_new_document", new_doc);
        let title = made.pointer("/created/title").and_then(|t| t.as_str()).map(String::from);
        let path = made.pointer("/documentIdentity/path").and_then(|t| t.as_str()).map(String::from);
        if let Some(k) = path.as_deref().or(title.as_deref()) {
            record_doc(k);
        }
        if let Some(title) = title {
            // BY PATH, never by title (0.10.10). MEASURED on Michael's SOLIDWORKS
            // 2021: the six verbs below were aimed at "Part14-….SLDPRT" while
            // SOLIDWORKS — with Windows hiding known extensions, the default —
            // titled it "Part14-…", and every one failed document_not_open; the
            // close missed too and left the part open and dirty. A path is an
            // identity; a title is what Explorer settings make of one.
            let addr = match &path {
                Some(p) => json!({"path": p}),
                None => json!({"targetDocument": title}),
            };
            let target = |extra: Value| -> Value {
                let mut a = addr.clone();
                if let (Value::Object(m), Value::Object(e)) = (&mut a, extra) {
                    m.extend(e);
                }
                a
            };
            // Issue #2.b: scripted by PATH, it is the bridge's own.
            let by = match &path {
                Some(p) => json!({"path": p}),
                None => json!({"targetDocument": title}),
            };
            let mut sargs = by.clone();
            sargs["script"] = json!([
                {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},
                {"op": "extrude", "depthMm": 10}
            ]);
            let block = run.call(ctx, "T23 issue #2: a script into the bridge's own part (by path) says bridgeCreatedDocument:true", "swx_run_script", sargs);
            if block.get("success").and_then(|v| v.as_bool()) != Some(false) && block.get("bridgeCreatedDocument") != Some(&json!(true)) {
                if let Some(last) = run.results.last_mut() {
                    last["pass"] = json!(false);
                    last["error"] = json!(format!("bridgeCreatedDocument = {} for the bridge's own part", block.get("bridgeCreatedDocument").cloned().unwrap_or(Value::Null)));
                }
            }
            let boss = block.pointer("/steps/1/result/feature").and_then(|f| f.as_str()).map(String::from);
            run.call(ctx, "design info", "swx_design_info", target(json!({})));
            run.call(ctx, "parameters", "swx_get_parameters", target(json!({})));
            match &boss {
                Some(b) => {
                    run.call(ctx, "set a dimension", "swx_set_parameter", target(json!({"name": format!("D1@{b}"), "valueMm": 12})));
                }
                None => run.skip("set a dimension", "swx_set_parameter", "the extrude made no feature"),
            }
            let out = crate::scratch::exports_dir().join(format!("selftest-{}.step", crate::scratch::next_scratch_seq()));
            run.call(ctx, "export a STEP copy", "swx_export_step", target(json!({"outputPath": out.to_string_lossy()})));
            run.call(ctx, "save the scratch part", "swx_save", target(json!({})));
            let closed = run.call(ctx, "close the scratch part", "swx_close_doc", target(json!({"save": false})));
            if closed.get("success").and_then(|v| v.as_bool()) != Some(false) {
                forget_doc(path.as_deref().unwrap_or(&title));
            } else {
                // A close that missed must not leave the part open SILENTLY:
                // once more by whichever name was not used, then say so.
                let again_failed = path.is_none()
                    || crate::verbs::dispatch(ctx, "swx_close_doc", &json!({"targetDocument": title, "save": false}))
                        .get("success")
                        .and_then(|v| v.as_bool())
                        == Some(false);
                if !again_failed {
                    forget_doc(path.as_deref().unwrap_or(&title));
                } else {
                    left_open.push(json!({"title": title, "path": path, "why": "close_failed",
                                          "error": closed.get("error").cloned().unwrap_or(Value::Null)}));
                }
            }
        } else {
            run.skip("the verbs on a scratch part", "swx_new_document", "the scratch part could not be created (see the row above)");
        }
        if let Some(prev) = &active {
            run.call(ctx, "re-activate the document that was active before", "swx_activate_document", json!({"name": prev}));
        }
        // HYGIENE (0.10.7): release every COM reference the run held. MEASURED
        // 2026-10-14: a respawn freed 210 MB once and 0 MB once — never touching
        // SOLIDWORKS, and not a saving to rely on; what it freed is reported.
        memory_log.com_respawn = Some(respawn_com_host(ctx));
        let mut extra = memory_log.to_json(memory_override.as_deref());
        if !left_open.is_empty() {
            extra["documentsLeftOpen"] = Value::Array(left_open);
        }
        return finish_with(level, &sw, ready, true, started, run, None, extra);
    }

    finish(level, &sw, ready, true, started, run, None)
}

/// `level:"cswa"` (0.11.0): every shipped CSWA spec (resources/cswa) built in
/// a part of its own (each sets its material, which outlives clear_part),
/// weighed with swx_mass_properties, compared with the published key within
/// its tolerance, and closed. The same presence and memory rules as the write
/// level stop the run between parts. `only:[ids]` runs a subset.
fn cswa_level(ctx: &Ctx, args: &Value, sw: &Value, ready: bool, started: Instant, mut run: Run, active: Option<String>) -> Value {
    let only: Vec<String> = args
        .get("only")
        .and_then(|o| o.as_array())
        .map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
        .unwrap_or_default();
    let memory_override = crate::memory::override_reason(args).ok().flatten();
    let idle_wait = crate::presence::wait_for_idle_secs(args);
    let mut memory_log = MemoryLog::default();
    let mut none: Option<String> = None;
    let mut last: Option<String> = None;
    let (mut passed, mut ran, mut skipped, mut simulator_only) = (0usize, 0usize, 0usize, 0usize);
    let mut parts: Vec<Value> = Vec::new();
    for spec in crate::cswa::specs() {
        let spec = match spec {
            Ok(s) => s,
            Err(e) => {
                run.results.push(json!({"name": "a CSWA spec", "verb": "swx_run_script", "pass": false, "error": e}));
                continue;
            }
        };
        if !only.is_empty() && !only.iter().any(|o| *o == spec.id) {
            continue;
        }
        let name = format!("CSWA {}: {}", spec.id, spec.title);
        if !spec.needs_ops.is_empty() {
            skipped += 1;
            run.skip(&name, "swx_run_script", &format!("needs ops the bridge does not have yet: {}", spec.needs_ops.join(", ")));
            continue;
        }
        // A part only SOLIDWORKS can build (0.11.4: Offset Entities reads the
        // face's own outline) is skipped in the SIMULATOR, saying why — and
        // built like any other on a real install.
        if let (true, Some(why)) = (crate::is_mock(), &spec.simulator_skips) {
            simulator_only += 1;
            run.skip(&name, "swx_run_script", &format!("skipped in the simulator: {why}"));
            continue;
        }
        let id = spec.id.as_str();
        if let Err(stop) = presence_between(ctx, last.as_deref(), id, idle_wait, &mut none) {
            memory_log.stopped_for = Some("userPresent");
            memory_log.stopped = Some(stop);
            break;
        }
        if let Err(stop) = memory_between(ctx, last.as_deref(), id, memory_override.as_deref(), &mut memory_log, &mut none) {
            memory_log.stopped = Some(stop);
            break;
        }
        ran += 1;
        let t = Instant::now();
        let mut sargs = json!({"script": spec.script});
        if let Some(r) = &memory_override {
            sargs["ignoreMemoryPressure"] = json!(true);
            sargs["reason"] = json!(r);
        }
        let built = crate::verbs::dispatch(ctx, "swx_run_script", &sargs);
        memory_log.documents_created += 1;
        let doc = built.get("documentName").and_then(|d| d.as_str()).map(String::from)
            .or_else(|| built.pointer("/created/title").and_then(|d| d.as_str()).map(String::from));
        let mut row = json!({"name": name, "verb": "swx_run_script", "official": spec.official, "tolerancePct": spec.tol_pct});
        if built.get("success").and_then(|v| v.as_bool()) == Some(false) {
            row["pass"] = json!(false);
            row["errorCode"] = built.get("errorCode").cloned().unwrap_or(Value::Null);
            row["error"] = json!(built.get("error").and_then(|e| e.as_str()).unwrap_or("").chars().take(400).collect::<String>());
        } else {
            let mp = crate::verbs::dispatch(ctx, "swx_mass_properties", &json!({"targetDocument": doc}));
            let (ok, checks) = crate::cswa::compare(&spec, &mp);
            if ok {
                passed += 1;
            }
            row["pass"] = json!(ok);
            row["checks"] = checks;
            row["densityKgM3"] = mp.get("densityKgM3").or_else(|| mp.get("density")).cloned().unwrap_or(Value::Null);
            row["materialName"] = mp.get("materialName").cloned().unwrap_or(Value::Null);
        }
        row["ms"] = json!(t.elapsed().as_millis() as u64);
        parts.push(json!({"id": spec.id, "pass": row["pass"].clone()}));
        run.results.push(row);
        if let Some(d) = doc {
            let _ = crate::verbs::dispatch(ctx, "swx_close_doc", &json!({"targetDocument": d, "save": false}));
        }
        last = Some(spec.id.clone());
    }
    if let Some(prev) = &active {
        run.call(ctx, "re-activate the document that was active before", "swx_activate_document", json!({"name": prev}));
    }
    let verdict = crate::cswa::summary(passed, ran, skipped, simulator_only);
    let mut extra = memory_log.to_json(memory_override.as_deref());
    extra["cswa"] = json!({"summary": verdict, "passed": passed, "ran": ran, "skipped": skipped + simulator_only, "skippedInSimulator": simulator_only, "parts": parts,
                           "allPassed": ran > 0 && passed == ran && memory_log.stopped.is_none()});
    finish_with("cswa", sw, ready, true, started, run, Some(format!("{verdict}.")), extra)
}

/// What the memory checks between the geometry checks did.
#[derive(Default)]
struct MemoryLog {
    /// Why the run stopped early: "memory" (the default) or "userPresent".
    stopped_for: Option<&'static str>,
    /// Documents the run created (the shared part counts once).
    documents_created: usize,
    /// Checks that ran under HIGH commit pressure with SOLIDWORKS within budget.
    pressure_seen: Vec<Value>,
    /// The COM host respawned at the end of the run, and what it freed.
    com_respawn: Option<Value>,
    recycled: Vec<Value>,
    /// Checks run on under `ignoreMemoryPressure` where the run would have stopped.
    overridden: Vec<Value>,
    stopped: Option<Value>,
}

impl MemoryLog {
    fn to_json(&self, over: Option<&str>) -> Value {
        let mut v = json!({
            "recycled": self.recycled,
            "documentsCreated": self.documents_created,
            "memoryPressureSeen": self.pressure_seen,
            "comRespawn": self.com_respawn,
            "complete": self.stopped.is_none(),
            "memory": crate::memory::report(crate::launchos::os().as_ref()),
        });
        if let Some(stop) = &self.stopped {
            v["stoppedFor"] = json!(self.stopped_for.unwrap_or("memory"));
            v["stopped"] = stop.clone();
        }
        if let Some(r) = over {
            v["memoryOverride"] = json!({"reason": r, "continuedWhereItWouldHaveStopped": self.overridden});
        }
        v
    }
}

/// Before one geometry check: is the user at the machine? (0.10.9) Wait up to
/// `wait` seconds for them to go idle; if they do not, close the run's own
/// shared part (its scratch — never theirs) and STOP. Never plough on: the
/// measured alternative was 18 of the bridge's windows on Drew's screen.
fn presence_between(ctx: &Ctx, last: Option<&str>, next: &str, wait: u64, shared: &mut Option<String>) -> Result<(), Value> {
    match crate::presence::wait_for_idle(wait) {
        Ok(_) => Ok(()),
        Err(a) => {
            close_shared(ctx, shared);
            crate::ledger::record("user_present_refusal", json!({"verb": "swx_selftest", "beforeCheck": next, "waitedSeconds": wait}));
            Err(json!({
                "beforeCheck": next,
                "afterCheck": last,
                "why": format!(
                    "the user is at the machine (input {} ago) and stayed active for the {wait} s the run waited; \
                     the run closed its own scratch part and stopped rather than open more windows on them",
                    a.idle_seconds.map(|n| format!("{n} s")).unwrap_or_else(|| "at an unreadable time".into())
                ),
                "userPresence": a.to_json(),
            }))
        }
    }
}

/// Before one geometry check: read memory, and recycle or stop if it calls
/// for it. `Err` stops the run, with everything that explains why.
fn memory_between(
    ctx: &Ctx,
    last: Option<&str>,
    next: &str,
    over: Option<&str>,
    log: &mut MemoryLog,
    shared: &mut Option<String>,
) -> Result<(), Value> {
    use crate::memory::Between;
    let os = crate::launchos::os();
    let s = crate::memory::sample(os.as_ref());
    let (p, pressure_why) = s.pressure();
    if !crate::memory::needs_attention(p, s.sw_private_mb, s.budget_mb) {
        // HIGH, with SOLIDWORKS within its budget: carry on, and report it
        // (0.10.7 — 0.10.6 stopped a run for memory other applications held).
        if let crate::memory::Between::Note(why) = crate::memory::between_checks(p, s.sw_private_mb, s.budget_mb, None, None) {
            if log.pressure_seen.len() < 50 {
                log.pressure_seen.push(json!({"beforeCheck": next, "pressure": p.name(), "why": why, "readings": pressure_why}));
            }
        }
        return Ok(());
    }
    // The run's own shared part is closed first: it is the bridge's, and a
    // session can only be recycled with nothing open.
    close_shared(ctx, shared);
    let facts = crate::memory::session_facts(ctx, os.as_ref());
    match crate::memory::between_checks(p, s.sw_private_mb, s.budget_mb, facts.bridge_started, facts.open_docs) {
        Between::Continue => Ok(()),
        Between::Note(why) => {
            log.pressure_seen.push(json!({"beforeCheck": next, "pressure": p.name(), "why": why}));
            Ok(())
        }
        Between::Recycle(why) => {
            let r = recycle(ctx, last, next, &s, &why)?;
            log.recycled.push(r);
            Ok(())
        }
        Between::Stop(why) => {
            if over.is_some() {
                log.overridden.push(json!({"beforeCheck": next, "why": why}));
                return Ok(());
            }
            Err(json!({
                "beforeCheck": next,
                "afterCheck": last,
                "why": why,
                "solidworksSession": {"pid": facts.com_pid, "bridgeStarted": facts.bridge_started, "openDocuments": facts.open_docs},
                "remedy": crate::memory::remedy(facts.bridge_started, facts.open_docs, s.sw_private_mb),
            }))
        }
    }
}

/// Can this check run in the SHARED, cleared document? Not if it sets a
/// material, colour or property (they outlive clear_part), names a feature by
/// its number (numbering continues in a reused part), or checks the feature
/// list — those get a fresh part of their own.
fn reusable(c: &Check) -> bool {
    let Some(steps) = c.script.as_array() else { return false };
    if steps.first().and_then(|s| s.get("op")).and_then(|o| o.as_str()) != Some("new_part") || c.expect.features.is_some() {
        return false;
    }
    steps.iter().skip(1).all(|st| {
        let op = st.get("op").and_then(|o| o.as_str()).unwrap_or("");
        if matches!(op, "set_material" | "set_appearance" | "set_prop" | "new_part" | "new_assembly" | "save_as") {
            return false;
        }
        let named = |v: Option<&Value>| v.and_then(|f| f.as_str()).map(|f| f != "last").unwrap_or(false);
        !(named(st.get("feature")) || named(st.pointer("/edges/allOfFeature")))
    })
}

/// The battery's shared part: VISIBLE (a hidden one cannot be modelled), the
/// bridge's own.
fn make_shared_document(ctx: &Ctx, over: Option<&str>) -> Option<String> {
    let mut a = json!({"type": "part"});
    if let Some(r) = over {
        a["ignoreMemoryPressure"] = json!(true);
        a["reason"] = json!(r);
    }
    let made = crate::verbs::dispatch(ctx, "swx_new_document", &a);
    let key = made.pointer("/documentIdentity/path")
        .or_else(|| made.pointer("/created/path"))
        .and_then(|p| p.as_str())
        .filter(|p| !p.is_empty())
        .or_else(|| made.pointer("/created/title").and_then(|t| t.as_str()))
        .map(String::from);
    if let Some(k) = &key {
        record_doc(k);
    }
    key
}

fn close_shared(ctx: &Ctx, shared: &mut Option<String>) {
    if let Some(t) = shared.take() {
        let r = crate::verbs::dispatch(ctx, "swx_close_doc", &close_args(&t));
        if r.get("success").and_then(|v| v.as_bool()) != Some(false) {
            forget_doc(&t);
        }
    }
}

/// swx_com_respawn at the end of a write run, with SOLIDWORKS's memory read
/// either side of it.
fn respawn_com_host(ctx: &Ctx) -> Value {
    let before = crate::memory::sample(crate::launchos::os().as_ref());
    let r = crate::verbs::dispatch(
        ctx,
        "swx_com_respawn",
        &json!({"reason": "swx_selftest: release the COM references the run held"}),
    );
    let after = crate::memory::sample(crate::launchos::os().as_ref());
    json!({
        "respawned": r.get("respawned").cloned().unwrap_or(json!(false)),
        "gracefulExit": r.get("gracefulExit").cloned().unwrap_or(Value::Null),
        "solidworksPrivateMbBefore": before.sw_private_mb,
        "solidworksPrivateMbAfter": after.sw_private_mb,
        "freedMb": match (before.sw_private_mb, after.sw_private_mb) {
            (Some(b), Some(a)) => json!(b as i64 - a as i64),
            _ => Value::Null,
        },
        "error": r.get("error").cloned().unwrap_or(Value::Null),
    })
}

fn compact(v: &Value) -> Value {
    json!({
        "success": v.get("success"),
        "errorCode": v.get("errorCode"),
        "error": v.get("error"),
        "stopped": v.get("stopped"),
        "winningRung": v.get("winningRung"),
        "attached": v.get("attached"),
        "stillLaunching": v.get("stillLaunching"),
    })
}

fn env_u64(name: &str, default: u64) -> u64 {
    std::env::var(name).ok().and_then(|v| v.trim().parse().ok()).unwrap_or(default)
}

/// RECYCLE THE BRIDGE'S OWN, EMPTY SESSION: swx_stop (every one of its own
/// refusals intact — documents, dialogs, the user's presence, identity), then
/// swx_start, then wait until SOLIDWORKS is ready. Anything short of that STOPS
/// the run.
fn recycle(ctx: &Ctx, last: Option<&str>, next: &str, before: &crate::memory::Sample, why: &str) -> Result<Value, Value> {
    let t = Instant::now();
    let reason = format!("swx_selftest is recycling the bridge's own SOLIDWORKS session before {next}: {why}");
    let fail = |at: &str, detail: Value| {
        json!({"beforeCheck": next, "afterCheck": last, "why": why, "recycleFailedAt": at, "detail": detail})
    };
    let stop = crate::verbs::dispatch(ctx, "swx_stop", &json!({"userAuthorized": true, "reason": reason}));
    if stop.get("stopped").and_then(|v| v.as_bool()) != Some(true) {
        return Err(fail("swx_stop", compact(&stop)));
    }
    let start = crate::verbs::dispatch(ctx, "swx_start", &json!({"reason": reason, "waitSeconds": 240}));
    let budget = std::time::Duration::from_secs(env_u64("SWX_RECYCLE_READY_SECS", 360));
    let poll = std::time::Duration::from_millis(env_u64("SWX_RECYCLE_POLL_MS", 3000));
    loop {
        let r = crate::readiness::assess(ctx);
        if r.get("ready").and_then(|v| v.as_bool()) == Some(true) {
            break;
        }
        if t.elapsed() >= budget {
            return Err(fail("ready", json!({"swxStart": compact(&start), "readiness": r})));
        }
        std::thread::sleep(poll);
    }
    let after = crate::memory::sample(crate::launchos::os().as_ref());
    let freed = match (before.commit_avail_mb(), after.commit_avail_mb()) {
        (Some(b), Some(a)) => Some(a as i64 - b as i64),
        _ => match (before.sw_private_mb, after.sw_private_mb) {
            (Some(b), Some(a)) => Some(b as i64 - a as i64),
            (Some(b), None) => Some(b as i64),
            _ => None,
        },
    };
    let row = json!({
        "afterCheck": last,
        "beforeCheck": next,
        "freedMb": freed,
        "why": why,
        "solidworksPrivateMbBefore": before.sw_private_mb,
        "solidworksPrivateMbAfter": after.sw_private_mb,
        "commitAvailableMbBefore": before.commit_avail_mb(),
        "commitAvailableMbAfter": after.commit_avail_mb(),
        "seconds": t.elapsed().as_secs(),
    });
    crate::ledger::record("solidworks_recycled", row.clone());
    Ok(row)
}

/// The reply: the rows, the counts, and — when SOLIDWORKS was not there —
/// that FIRST, so nobody reads a skipped check as a failure.
fn finish(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>) -> Value {
    finish_with(level, sw, ready, running, started, run, lead, json!({}))
}

/// `finish`, with the write level's memory record merged in (0.10.5).
#[allow(clippy::too_many_arguments)]
fn finish_with(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>, extra: Value) -> Value {
    let recycled = extra.get("recycled").and_then(|r| r.as_array()).map(|a| a.len()).unwrap_or(0);
    let pressured = extra.get("memoryPressureSeen").and_then(|r| r.as_array()).map(|a| a.len()).unwrap_or(0);
    let lead = match pressured {
        0 => lead,
        n => Some(format!(
            "MEMORY: committed memory was HIGH before {n} check(s), held by other applications — SOLIDWORKS stayed \
             within its budget, so the run carried on (see `memoryPressureSeen` and swx_status.memory). {}",
            lead.unwrap_or_default()
        )),
    };
    let lead = match recycled {
        0 => lead,
        n => Some(format!(
            "MEMORY: this run recycled the bridge's own SOLIDWORKS session {} (see `recycled`: when, \
             why, and what it freed). {}",
            if n == 1 { "once".to_string() } else { format!("{n} times") },
            lead.unwrap_or_default()
        )),
    };
    let lead = match extra.get("stopped") {
        Some(stop) => Some(format!(
            "THE RUN STOPPED {} before {}: {}. Every row above that point is a real result; \
             nothing after it ran. {}",
            if extra.get("stoppedFor").and_then(|v| v.as_str()) == Some("userPresent") {
                "BECAUSE THE USER IS AT THE MACHINE"
            } else {
                "FOR MEMORY"
            },
            stop["beforeCheck"].as_str().unwrap_or("the next check"),
            stop["why"].as_str().unwrap_or("memory"),
            lead.unwrap_or_default()
        )),
        None => lead,
    };
    let mut out = finish_base(level, sw, ready, running, started, run, lead);
    if let (Value::Object(m), Value::Object(e)) = (&mut out, extra) {
        for (k, v) in e {
            m.insert(k, v);
        }
    }
    out
}

fn finish_base(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>) -> Value {
    let passed = run.results.iter().filter(|r| r["pass"] == true).count();
    let failed = run.results.iter().filter(|r| r["pass"] == false).count();
    let skipped = run.results.iter().filter(|r| r["pass"].is_null()).count();
    let send = "Send this WHOLE reply back (for example on the bridge's wiki issue): each row is one verb or one modelling op on your install, with the error SOLIDWORKS gave when it failed. level:\"read\" changed nothing; level:\"write\" built, exported, saved and closed a scratch part in the bridge's own scratch folder and re-activated the document that was active before.";
    let hint = match lead {
        Some(first) => format!("{first} {send}"),
        None => send.to_string(),
    };
    // The SAME table step results' `proven` comes from (compat::OP_FACTS),
    // each row saying whether it is live on THIS install's release.
    let major = sw.pointer("/versionSupport/majorVersion").and_then(|m| m.as_u64()).map(|m| m as u32);
    crate::verbs::ok(json!({
        "level": level,
        "bridgeVersion": crate::BRIDGE_VERSION,
        "liveMeasured": crate::compat::live_table(major),
        "solidworks": {
            "versionSupport": sw.get("versionSupport"),
            "edition": sw.get("edition"),
            "launchRoute": sw.get("launchRoute"),
            "registryYears": sw.get("registryYears"),
            "classicYears": sw.get("classicYears"),
            "ready": ready,
            "running": running,
        },
        "summary": {"passed": passed, "failed": failed, "skipped": skipped, "solidworksRunning": running, "seconds": started.elapsed().as_secs_f64()},
        "results": run.results,
        "_hint": hint,
        "_timeoutHint": TIMEOUT_HINT,
    }))
}

// ── the geometric proof ─────────────────────────────────────────────────────

const PI: f64 = std::f64::consts::PI;
const REL_TOL: f64 = 2e-3; // volumes: 0.2 %
const MM_TOL: f64 = 0.01; // bounding boxes: 0.01 mm

type Box3 = ([f64; 3], [f64; 3]);

fn bx(x0: f64, x1: f64, y0: f64, y1: f64, z0: f64, z1: f64) -> Box3 {
    ([x0, y0, z0], [x1, y1, z1])
}

fn cyl(d: f64, h: f64) -> f64 {
    PI * d * d / 4.0 * h
}

/// A box a x b x c with all 12 edges filleted r: the inner box Minkowski-summed
/// with a sphere of radius r.
fn rounded_box_volume(a: f64, b: f64, c: f64, r: f64) -> f64 {
    let (ai, bi, ci) = (a - 2.0 * r, b - 2.0 * r, c - 2.0 * r);
    ai * bi * ci + 2.0 * r * (ai * bi + bi * ci + ai * ci) + PI * r * r * (ai + bi + ci) + 4.0 / 3.0 * PI * r.powi(3)
}

/// A cylinder d x h with both circular edges filleted r (Pappus on the
/// spandrel, area (1 − π/4) r², centroid r(5/6 − π/4)/(1 − π/4) in from the wall).
fn fillet_cylinder_volume(d: f64, h: f64, r: f64) -> f64 {
    let big_r = d / 2.0;
    let area = (1.0 - PI / 4.0) * r * r;
    let xbar = r * (5.0 / 6.0 - PI / 4.0) / (1.0 - PI / 4.0);
    PI * big_r * big_r * h - 2.0 * area * 2.0 * PI * (big_r - xbar)
}

#[derive(Default)]
struct Expect {
    volume: Option<f64>,
    bbox: Option<Box3>,
    body_count: Option<usize>,
    face_count: Option<u64>,
    appearance: Option<[i64; 3]>,
    mass_kg: Option<f64>,
    density: Option<f64>,
    mass_matches_mp: bool,
    features: Option<Vec<&'static str>>,
    /// The run must FAIL at a cut (or, if it succeeds, must have changed the volume).
    expect_error_unless_volume_changed: Option<f64>,
    z_span: Option<f64>,
    xy: Option<[[f64; 2]; 2]>,
    body_boxes: Option<Vec<Box3>>,
    step_read_back: Option<usize>,
}

struct Check {
    id: &'static str,
    title: &'static str,
    script: Value,
    expect: Expect,
}

fn base() -> Vec<Value> {
    vec![
        json!({"op": "new_part"}),
        json!({"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"}),
        json!({"op": "extrude", "depthMm": 10}),
    ]
}

fn with_base(more: Value) -> Value {
    let mut v = base();
    if let Value::Array(a) = more {
        v.extend(a);
    }
    Value::Array(v)
}

/// The checks — the same as tools/dsl-live-check.py, expectations computed here.
fn battery() -> Vec<Check> {
    let base_box = bx(-10.0, 10.0, 0.0, 10.0, -10.0, 10.0);
    let lego_script = {
        let p = json!([
            {"op": "new_part"},
            {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 31.8, "heightMm": 15.8, "anchor": "center"},
            {"op": "extrude", "depthMm": 9.6},
            {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 29.4, "heightMm": 13.4, "anchor": "center"},
            {"op": "cut", "depthMm": 8.6, "reverse": true},
            {"op": "sketch", "plane": "Top Plane", "entities": [
                {"circle": {"centerMm": [-8, 0], "diameterMm": 6.51}}, {"circle": {"centerMm": [-8, 0], "diameterMm": 4.8}},
                {"circle": {"centerMm": [0, 0], "diameterMm": 6.51}}, {"circle": {"centerMm": [0, 0], "diameterMm": 4.8}},
                {"circle": {"centerMm": [8, 0], "diameterMm": 6.51}}, {"circle": {"centerMm": [8, 0], "diameterMm": 4.8}}
            ]},
            {"op": "extrude", "depthMm": 8.6},
            {"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": 9.6}, "entities": [
                {"circle": {"centerMm": [-12, -4], "diameterMm": 4.8}}, {"circle": {"centerMm": [-12, 4], "diameterMm": 4.8}},
                {"circle": {"centerMm": [-4, -4], "diameterMm": 4.8}}, {"circle": {"centerMm": [-4, 4], "diameterMm": 4.8}},
                {"circle": {"centerMm": [4, -4], "diameterMm": 4.8}}, {"circle": {"centerMm": [4, 4], "diameterMm": 4.8}},
                {"circle": {"centerMm": [12, -4], "diameterMm": 4.8}}, {"circle": {"centerMm": [12, 4], "diameterMm": 4.8}}
            ]},
            {"op": "extrude", "depthMm": 1.7},
            {"op": "set_material", "name": "ABS"},
            {"op": "set_appearance", "rgb": [200, 0, 0]}
        ]);
        p
    };
    let lego_volume = 31.8 * 15.8 * 9.6 - 29.4 * 13.4 * 8.6 + 3.0 * (cyl(6.51, 8.6) - cyl(4.8, 8.6)) + 8.0 * cyl(4.8, 1.7);
    vec![
        Check { id: "T01", title: "rectangle anchor: xMm/yMm is the CORNER by default",
            script: json!([{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "xMm": 0, "yMm": 0, "widthMm": 10, "heightMm": 20}, {"op": "extrude", "depthMm": 5}]),
            expect: Expect { volume: Some(1000.0), bbox: Some(bx(0.0, 10.0, 0.0, 20.0, 0.0, 5.0)), ..Default::default() } },
        Check { id: "T02", title: "Top Plane: sketch +Y is model -Z",
            script: json!([{"op": "new_part"}, {"op": "sketch_rect", "plane": "Top Plane", "xMm": 0, "yMm": 0, "widthMm": 10, "heightMm": 20}, {"op": "extrude", "depthMm": 5}]),
            expect: Expect { volume: Some(1000.0), bbox: Some(bx(0.0, 10.0, 0.0, 5.0, -20.0, 0.0)), ..Default::default() } },
        Check { id: "T03", title: "anchor center",
            script: json!([{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 20, "anchor": "center"}, {"op": "extrude", "depthMm": 5}]),
            expect: Expect { volume: Some(1000.0), bbox: Some(bx(-5.0, 5.0, -10.0, 10.0, 0.0, 5.0)), ..Default::default() } },
        Check { id: "T04", title: "sketch on a plane offset +10 mm from Top Plane",
            script: with_base(json!([{"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": 10}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 10}}]}, {"op": "extrude", "depthMm": 5}])),
            expect: Expect { volume: Some(4000.0 + cyl(10.0, 5.0)), bbox: Some(bx(-10.0, 10.0, 0.0, 15.0, -10.0, 10.0)), ..Default::default() } },
        Check { id: "T05", title: "sketch on a NEGATIVE offset plane (-5 mm)",
            script: with_base(json!([{"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": -5}, "entities": [{"rect": {"widthMm": 20, "heightMm": 20, "anchor": "center"}}]}, {"op": "extrude", "depthMm": 5}])),
            expect: Expect { volume: Some(6000.0), bbox: Some(bx(-10.0, 10.0, -5.0, 10.0, -10.0, 10.0)), ..Default::default() } },
        Check { id: "T06", title: "sketch on a FACE picked by a point (the top face)",
            script: with_base(json!([{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 6}}]}, {"op": "extrude", "depthMm": 4}])),
            expect: Expect { volume: Some(4000.0 + cyl(6.0, 4.0)), bbox: Some(bx(-10.0, 10.0, 0.0, 14.0, -10.0, 10.0)), ..Default::default() } },
        Check { id: "T07", title: "a default cut from Top Plane with the body on +Y fails LOUDLY, never silently",
            script: with_base(json!([{"op": "sketch_rect", "plane": "Top Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"}, {"op": "cut", "depthMm": 5}])),
            expect: Expect { expect_error_unless_volume_changed: Some(4000.0), ..Default::default() } },
        Check { id: "T08", title: "cut reverse:true removes material on +Y",
            script: with_base(json!([{"op": "sketch_rect", "plane": "Top Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"}, {"op": "cut", "depthMm": 5, "reverse": true}])),
            expect: Expect { volume: Some(3500.0), bbox: Some(base_box), ..Default::default() } },
        Check { id: "T09", title: "cut throughAll + reverse",
            script: with_base(json!([{"op": "sketch_rect", "plane": "Top Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"}, {"op": "cut", "throughAll": true, "reverse": true}])),
            expect: Expect { volume: Some(3000.0), bbox: Some(base_box), ..Default::default() } },
        Check { id: "T10", title: "extrude direction both (5 mm one way, 3 mm the other)",
            script: json!([{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"}, {"op": "extrude", "depthMm": 5, "direction": "both", "depth2Mm": 3}]),
            expect: Expect { volume: Some(800.0), z_span: Some(8.0), xy: Some([[-5.0, 5.0], [-5.0, 5.0]]), ..Default::default() } },
        Check { id: "T11", title: "extrude midPlane",
            script: json!([{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"}, {"op": "extrude", "depthMm": 10, "midPlane": true}]),
            expect: Expect { volume: Some(1000.0), bbox: Some(bx(-5.0, 5.0, -5.0, 5.0, -5.0, 5.0)), ..Default::default() } },
        Check { id: "T12", title: "cut midPlane through the block",
            script: with_base(json!([{"op": "sketch_rect", "plane": "Front Plane", "widthMm": 4, "heightMm": 4, "anchor": "center", "yMm": 5}, {"op": "cut", "depthMm": 6, "midPlane": true}])),
            expect: Expect { volume: Some(4000.0 - 96.0), bbox: Some(base_box), ..Default::default() } },
        Check { id: "T13", title: "two concentric circles in ONE sketch extrude as a RING",
            script: json!([{"op": "new_part"}, {"op": "sketch", "plane": "Top Plane", "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 10}}, {"circle": {"centerMm": [0, 0], "diameterMm": 6}}]}, {"op": "extrude", "depthMm": 10}]),
            expect: Expect { volume: Some(cyl(10.0, 10.0) - cyl(6.0, 10.0)), face_count: Some(4), body_count: Some(1), ..Default::default() } },
        Check { id: "T14", title: "two disjoint rectangles in ONE sketch: one feature, two bodies",
            script: json!([{"op": "new_part"}, {"op": "sketch", "plane": "Front Plane", "entities": [{"rect": {"xMm": 0, "yMm": 0, "widthMm": 5, "heightMm": 5}}, {"rect": {"xMm": 10, "yMm": 0, "widthMm": 5, "heightMm": 5}}]}, {"op": "extrude", "depthMm": 5}]),
            expect: Expect { volume: Some(250.0), body_count: Some(2), ..Default::default() } },
        Check { id: "T15", title: "set_appearance on the part",
            script: with_base(json!([{"op": "set_appearance", "rgb": [200, 0, 0]}])),
            expect: Expect { volume: Some(4000.0), appearance: Some([200, 0, 0]), ..Default::default() } },
        Check { id: "T16", title: "set_appearance on one face picked by a point",
            script: with_base(json!([{"op": "set_appearance", "rgb": [0, 0, 255], "target": {"faceAtPointMm": [0, 10, 0]}}])),
            expect: Expect { volume: Some(4000.0), step_read_back: Some(3), ..Default::default() } },
        Check { id: "T17", title: "fillet every edge of the block, r = 1 (allOfFeature)",
            script: with_base(json!([{"op": "fillet", "radiusMm": 1, "edges": {"allOfFeature": "Boss-Extrude1"}}])),
            expect: Expect { volume: Some(rounded_box_volume(20.0, 10.0, 20.0, 1.0)), bbox: Some(base_box), ..Default::default() } },
        Check { id: "T18", title: "chamfer ONE edge picked by a point, 1 mm",
            script: with_base(json!([{"op": "chamfer", "distanceMm": 1, "edges": {"atPointsMm": [[0, 10, 10]]}}])),
            expect: Expect { volume: Some(4000.0 - 0.5 * 1.0 * 1.0 * 20.0), bbox: Some(base_box), ..Default::default() } },
        Check { id: "T19", title: "shell 1 mm, top face removed",
            script: with_base(json!([{"op": "shell", "thicknessMm": 1, "removeFacesAtPointsMm": [[0, 10, 0]]}])),
            expect: Expect { volume: Some(4000.0 - 18.0 * 18.0 * 9.0), bbox: Some(base_box), ..Default::default() } },
        Check { id: "T20", title: "linear pattern: one boss -> 3 along X, 6 mm apart",
            script: with_base(json!([{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [-6, 0], "diameterMm": 4}}]}, {"op": "extrude", "depthMm": 5},
                                     {"op": "linear_pattern", "feature": "Boss-Extrude2", "directions": [{"axis": "x", "count": 3, "spacingMm": 6}]}])),
            expect: Expect { volume: Some(4000.0 + 3.0 * cyl(4.0, 5.0)), bbox: Some(bx(-10.0, 10.0, 0.0, 15.0, -10.0, 10.0)), ..Default::default() } },
        Check { id: "T21", title: "circular pattern: one boss -> 6 around Y",
            script: with_base(json!([{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [7, 0], "diameterMm": 3}}]}, {"op": "extrude", "depthMm": 4},
                                     {"op": "circular_pattern", "feature": "Boss-Extrude2", "axis": "y", "count": 6}])),
            expect: Expect { volume: Some(4000.0 + 6.0 * cyl(3.0, 4.0)), ..Default::default() } },
        Check { id: "T22", title: "issue #2: body mass uses the material's density (ABS 1020 kg/m^3)",
            script: with_base(json!([{"op": "set_material", "name": "ABS"}])),
            expect: Expect { volume: Some(4000.0), mass_kg: Some(4000e-9 * 1020.0), density: Some(1020.0), mass_matches_mp: true, ..Default::default() } },
        Check { id: "T25", title: "sketch on the BOTTOM face (hidden in an iso view, normal -Y) extrudes OUTWARD",
            script: with_base(json!([{"op": "sketch", "face": {"atPointMm": [0, 0, 0]}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 6}}]}, {"op": "extrude", "depthMm": 3}])),
            expect: Expect { volume: Some(4000.0 + cyl(6.0, 3.0)), bbox: Some(bx(-10.0, 10.0, -3.0, 10.0, -10.0, 10.0)), ..Default::default() } },
        Check { id: "T26", title: "a NEGATIVE offset plane keeps the base plane's axes (asymmetric sketch, separate body)",
            script: with_base(json!([{"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": -5}, "entities": [{"rect": {"xMm": 0, "yMm": 0, "widthMm": 4, "heightMm": 6}}]}, {"op": "extrude", "depthMm": 2}])),
            expect: Expect { volume: Some(4048.0), body_count: Some(2), body_boxes: Some(vec![base_box, bx(0.0, 4.0, -5.0, -3.0, -6.0, 0.0)]), ..Default::default() } },
        Check { id: "T27", title: "an ASYMMETRIC two-way cut: depthMm along the cut's own direction (-normal), depth2Mm the other way",
            script: json!([{"op": "new_part"}, {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"}, {"op": "extrude", "depthMm": 10},
                           {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 4, "heightMm": 4, "anchor": "center"}, {"op": "cut", "depthMm": 6, "direction": "both", "depth2Mm": 2}]),
            expect: Expect { volume: Some(1000.0 - 4.0 * 4.0 * 2.0), bbox: Some(bx(-5.0, 5.0, -5.0, 5.0, 0.0, 10.0)), ..Default::default() } },
        Check { id: "T28", title: "linear pattern with NEGATIVE spacing goes -X (instances stay on the block)",
            script: with_base(json!([{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [6, 0], "diameterMm": 4}}]}, {"op": "extrude", "depthMm": 5},
                                     {"op": "linear_pattern", "feature": "Boss-Extrude2", "directions": [{"axis": "x", "count": 3, "spacingMm": -6}]}])),
            expect: Expect { volume: Some(4000.0 + 3.0 * cyl(4.0, 5.0)), body_count: Some(1), bbox: Some(bx(-10.0, 10.0, 0.0, 15.0, -10.0, 10.0)), ..Default::default() } },
        Check { id: "T29", title: "shell removing the BOTTOM face (hidden in an iso view)",
            script: with_base(json!([{"op": "shell", "thicknessMm": 1, "removeFacesAtPointsMm": [[0, 0, 0]]}])),
            expect: Expect { volume: Some(4000.0 - 18.0 * 18.0 * 9.0), bbox: Some(base_box), ..Default::default() } },
        Check { id: "T30", title: "fillet ONE feature's edges by allOfFeature on a cylinder (2 circular edges)",
            script: json!([{"op": "new_part"}, {"op": "sketch_circle", "plane": "Top Plane", "radiusMm": 5}, {"op": "extrude", "depthMm": 10},
                           {"op": "fillet", "radiusMm": 1, "edges": {"allOfFeature": "Boss-Extrude1"}}]),
            expect: Expect { volume: Some(fillet_cylinder_volume(10.0, 10.0, 1.0)), ..Default::default() } },
        Check { id: "T31", title: "a material AFTER an appearance keeps the colour (the bridge re-applies it)",
            script: with_base(json!([{"op": "set_appearance", "rgb": [0, 160, 60]}, {"op": "set_material", "name": "ABS"}])),
            expect: Expect { volume: Some(4000.0), appearance: Some([0, 160, 60]), ..Default::default() } },
        Check { id: "T32", title: "an op between a sketch and its extrude does not lose the sketch",
            script: with_base(json!([{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 6}}]},
                                     {"op": "fillet", "radiusMm": 0.5, "edges": {"atPointsMm": [[0, 0, 10]]}},
                                     {"op": "extrude", "depthMm": 4}])),
            expect: Expect { volume: Some(4000.0 - (1.0 - PI / 4.0) * 0.25 * 20.0 + cyl(6.0, 4.0)), ..Default::default() } },
        Check { id: "T24", title: "the LEGO 2x4 acceptance brick (examples/lego-2x4.json)",
            script: lego_script,
            expect: Expect { volume: Some(lego_volume), bbox: Some(bx(-15.9, 15.9, 0.0, 11.3, -7.9, 7.9)), appearance: Some([200, 0, 0]),
                             mass_kg: Some(lego_volume * 1e-9 * 1020.0), density: Some(1020.0), mass_matches_mp: true,
                             features: Some(vec!["Boss-Extrude1", "Cut-Extrude1", "Boss-Extrude2", "Boss-Extrude3"]), ..Default::default() } },
    ]
}

fn rel_ok(got: f64, want: f64) -> bool {
    (got - want).abs() <= REL_TOL * want.abs().max(1.0)
}

fn union_box(bodies: &[Value]) -> Option<Box3> {
    let mut lo = [f64::MAX; 3];
    let mut hi = [f64::MIN; 3];
    let mut any = false;
    for b in bodies {
        for i in 0..3 {
            if let (Some(l), Some(h)) = (b.pointer(&format!("/bbox/minMm/{i}")).and_then(|v| v.as_f64()), b.pointer(&format!("/bbox/maxMm/{i}")).and_then(|v| v.as_f64())) {
                lo[i] = lo[i].min(l);
                hi[i] = hi[i].max(h);
                any = true;
            }
        }
    }
    any.then_some((lo, hi))
}

/// The titles of the open documents (swx_docs' `docs`).
fn titles(ctx: &Ctx) -> Vec<String> {
    crate::verbs::dispatch(ctx, "swx_docs", &json!({}))
        .get("docs")
        .and_then(|d| d.as_array().cloned())
        .unwrap_or_default()
        .iter()
        .filter_map(|d| d.get("title").and_then(|t| t.as_str()).map(String::from))
        .collect()
}

/// One check: build, MEASURE, compare, close what it made.
/// What a check did with documents.
struct CheckDocs {
    /// The shared part could not be cleared: it is replaced, not reused again.
    clear_failed: bool,
    /// The check ran in a part of its own (a fresh document was created).
    fresh: bool,
}

/// The part named in "…still there: Sketch13)" — what clear_part left.
fn clear_leftover(error: &str) -> String {
    error
        .split("still there: ")
        .nth(1)
        .map(|t| t.trim_end_matches(')').trim().to_string())
        .filter(|t| !t.is_empty())
        .unwrap_or_else(|| error.chars().take(160).collect())
}

fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<&str>, reuse: Option<&str>) -> CheckDocs {
    let t = Instant::now();
    let before = titles(ctx);
    let with_override = |mut a: Value| -> Value {
        if let Some(r) = memory_override {
            a["ignoreMemoryPressure"] = json!(true);
            a["reason"] = json!(r);
        }
        a
    };
    let sargs = with_override(match reuse {
        // In the shared part: its new_part becomes clear_part.
        Some(doc) => json!({"script": c.script, "reuseDocument": doc}),
        // A part of its own (visible: a hidden part cannot be modelled).
        None => json!({"script": c.script}),
    });
    let mut r = crate::verbs::dispatch(ctx, "swx_run_script", &sargs);
    // THE SHARED PART COULD NOT BE CLEARED (0.10.8): that is not this check's
    // failure. MEASURED: three checks of 0.10.7's live run failed on a sketch
    // the PREVIOUS check left behind. The check runs again in a fresh part, and
    // the row says so; it fails only on its own geometry.
    let mut fallback: Option<String> = None;
    if reuse.is_some() {
        if let Some(e) = r.get("error").and_then(|e| e.as_str()).filter(|e| e.contains("(clear_part) failed")) {
            fallback = Some(format!("fresh part — clear_part left {}", clear_leftover(e)));
            r = crate::verbs::dispatch(ctx, "swx_run_script", &with_override(json!({"script": c.script})));
        }
    }
    let ok = r.get("success").and_then(|v| v.as_bool()) != Some(false);
    let doc = r.get("documentName").and_then(|d| d.as_str()).map(String::from);
    if reuse.is_none() || fallback.is_some() {
        if let Some(d) = doc.as_deref().or_else(|| r.pointer("/createdBeforeFailing/title").and_then(|t| t.as_str())) {
            record_doc(d);
        }
    }
    let mut fails: Vec<String> = Vec::new();
    let mut measured = json!({});
    let e = &c.expect;
    let inspect = |doc: &Option<String>| -> Value {
        let a = match doc {
            Some(d) => json!({"targetDocument": d}),
            None => json!({}),
        };
        crate::verbs::dispatch(ctx, "swx_inspect_bodies", &a)
    };
    if let Some(unchanged) = e.expect_error_unless_volume_changed {
        if ok {
            let ib = inspect(&doc);
            let vol: f64 = ib.get("bodies").and_then(|b| b.as_array()).map(|a| a.iter().filter_map(|b| b["volumeMm3"].as_f64()).sum()).unwrap_or(0.0);
            measured = json!({"volumeMm3": vol});
            if rel_ok(vol, unchanged) {
                fails.push(format!("the cut reported success but removed nothing (volume still {vol}) — a silent no-op"));
            }
        } else {
            let msg = r.get("error").and_then(|m| m.as_str()).unwrap_or("").to_string();
            measured = json!({"error": msg});
            if !msg.to_lowercase().contains("cut") {
                fails.push(format!("failed, but the error does not name the cut: {}", msg.chars().take(200).collect::<String>()));
            }
        }
    } else if !ok {
        fails.push(format!("swx_run_script failed: {} {}", r.get("errorCode").cloned().unwrap_or(Value::Null), r.get("error").and_then(|m| m.as_str()).unwrap_or("").chars().take(300).collect::<String>()));
    } else {
        let steps = r.get("steps").and_then(|s| s.as_array()).cloned().unwrap_or_default();
        for st in &steps {
            let op = st["op"].as_str().unwrap_or("");
            if matches!(op, "extrude" | "cut" | "fillet" | "chamfer" | "shell" | "linear_pattern" | "circular_pattern") && st["result"]["feature"].is_null() {
                fails.push(format!("step {} ({op}) returned no feature", st["step"]));
            }
        }
        if let Some(want) = &e.features {
            let made: Vec<String> = steps.iter().filter(|st| matches!(st["op"].as_str(), Some("extrude") | Some("cut"))).map(|st| st["result"]["feature"].as_str().unwrap_or("").to_string()).collect();
            if made != *want {
                fails.push(format!("features {made:?}, expected {want:?}"));
            }
        }
        if let Some(i) = e.step_read_back {
            if steps.get(i).map(|st| st["result"]["readBack"].is_null()).unwrap_or(true) {
                fails.push(format!("step {} has no confirming read-back", i + 1));
            }
        }
        let ib = inspect(&doc);
        let bodies = ib.get("bodies").and_then(|b| b.as_array()).cloned().unwrap_or_default();
        if bodies.is_empty() {
            fails.push(format!("swx_inspect_bodies found no body ({})", ib.get("error").and_then(|m| m.as_str()).unwrap_or("")));
        } else {
            let vol: f64 = bodies.iter().filter_map(|b| b["volumeMm3"].as_f64()).sum();
            let bb = union_box(&bodies);
            measured = json!({
                "volumeMm3": vol,
                "bodyCount": bodies.len(),
                "bboxMm": bb.map(|(l, h)| json!({"min": l, "max": h})),
                "faceCount": bodies.iter().map(|b| b["faceCount"].clone()).collect::<Vec<_>>(),
                "appearanceRgb": bodies[0]["appearanceRgb"],
                "massKg": bodies.iter().filter_map(|b| b["massKg"].as_f64()).sum::<f64>(),
                "densityKgM3": bodies[0]["densityKgM3"],
            });
            if let Some(v) = e.volume {
                if !rel_ok(vol, v) {
                    fails.push(format!("volume {vol:.4} mm^3, expected {v:.4}"));
                }
            }
            if let Some(n) = e.body_count {
                if bodies.len() != n {
                    fails.push(format!("{} bodies, expected {n}", bodies.len()));
                }
            }
            if let Some(f) = e.face_count {
                if bodies[0]["faceCount"].as_u64() != Some(f) {
                    fails.push(format!("faceCount {}, expected {f}", bodies[0]["faceCount"]));
                }
            }
            if let (Some((wl, wh)), Some((gl, gh))) = (e.bbox, bb) {
                for i in 0..3 {
                    if (gl[i] - wl[i]).abs() > MM_TOL || (gh[i] - wh[i]).abs() > MM_TOL {
                        fails.push(format!("bbox {} = [{}, {}], expected [{}, {}]", ["X", "Y", "Z"][i], gl[i], gh[i], wl[i], wh[i]));
                    }
                }
            }
            if let (Some(span), Some((gl, gh))) = (e.z_span, bb) {
                if ((gh[2] - gl[2]) - span).abs() > MM_TOL {
                    fails.push(format!("Z span {}, expected {span}", gh[2] - gl[2]));
                }
            }
            if let (Some(xy), Some((gl, gh))) = (e.xy, bb) {
                for i in 0..2 {
                    if (gl[i] - xy[i][0]).abs() > MM_TOL || (gh[i] - xy[i][1]).abs() > MM_TOL {
                        fails.push(format!("bbox {} = [{}, {}], expected {:?}", ["X", "Y"][i], gl[i], gh[i], xy[i]));
                    }
                }
            }
            if let Some(want) = &e.body_boxes {
                let mut got: Vec<Box3> = bodies.iter().filter_map(|b| union_box(std::slice::from_ref(b))).collect();
                for w in want {
                    match got.iter().position(|g| (0..3).all(|i| (g.0[i] - w.0[i]).abs() <= MM_TOL && (g.1[i] - w.1[i]).abs() <= MM_TOL)) {
                        Some(k) => {
                            got.remove(k);
                        }
                        None => fails.push(format!("no body with bbox {w:?}")),
                    }
                }
            }
            if let Some(rgb) = e.appearance {
                let got: Vec<i64> = bodies[0]["appearanceRgb"].as_array().map(|a| a.iter().filter_map(|v| v.as_i64()).collect()).unwrap_or_default();
                if got.len() != 3 || got.iter().zip(rgb.iter()).any(|(a, b)| (a - b).abs() > 2) {
                    fails.push(format!("appearanceRgb {got:?}, expected {rgb:?}"));
                }
            }
            if let Some(m) = e.mass_kg {
                let got: f64 = bodies.iter().filter_map(|b| b["massKg"].as_f64()).sum();
                if !rel_ok(got, m) {
                    fails.push(format!("massKg {got}, expected {m}"));
                }
            }
            if let Some(d) = e.density {
                if bodies[0]["densityKgM3"].as_f64().map(|g| (g - d).abs() > 0.5).unwrap_or(true) {
                    fails.push(format!("densityKgM3 {}, expected {d}", bodies[0]["densityKgM3"]));
                }
            }
            if e.mass_matches_mp {
                let a = match &doc {
                    Some(d) => json!({"targetDocument": d}),
                    None => json!({}),
                };
                let mp = crate::verbs::dispatch(ctx, "swx_mass_properties", &a);
                let got: f64 = bodies.iter().filter_map(|b| b["massKg"].as_f64()).sum();
                if !rel_ok(got, mp["massKg"].as_f64().unwrap_or(-1.0)) {
                    fails.push(format!("inspect_bodies mass {got} != swx_mass_properties mass {}", mp["massKg"]));
                }
            }
        }
    }
    // Close what this check made (only documents that were not open before it).
    for title in titles(ctx).into_iter().filter(|t| !before.contains(t)) {
        let r = crate::verbs::dispatch(ctx, "swx_close_doc", &json!({"targetDocument": title, "save": false}));
        if r.get("success").and_then(|v| v.as_bool()) != Some(false) {
            forget_doc(&title);
        }
    }
    let expected = json!({
        "volumeMm3": e.volume,
        "bboxMm": e.bbox.map(|(l, h)| json!({"min": l, "max": h})),
        "bodyCount": e.body_count,
        "faceCount": e.face_count,
        "appearanceRgb": e.appearance,
        "massKg": e.mass_kg,
        "densityKgM3": e.density,
        "zSpanMm": e.z_span,
        "bodyBoxes": e.body_boxes.as_ref().map(|v| v.iter().map(|(l, h)| json!({"min": l, "max": h})).collect::<Vec<_>>()),
        "features": e.features,
        "mustFailOrRemoveMaterial": e.expect_error_unless_volume_changed.is_some(),
    });
    run.results.push(json!({
        "name": format!("{} {}", c.id, c.title),
        "verb": "swx_run_script",
        "pass": fails.is_empty(),
        "ms": t.elapsed().as_millis() as u64,
        "expected": expected,
        "measured": measured,
        "fails": fails,
        "reuseFallback": fallback,
    }));
    CheckDocs { clear_failed: fallback.is_some(), fresh: reuse.is_none() || fallback.is_some() }
}