app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
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() }
}