app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
0.10.8 source
16 files changed
+371−61
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+23−1bridge.json+1−1docs/LIVE-RESULTS.md+3install.shpackage.json+1−1skills/solidworks-memory/SKILL.md+19−9src/comhost/com.rs+52−26src/comhost/mock.rs+60−6src/comhost/mock_model.rs+19−1src/modeling.rs+83src/selftest.rs+47−13tests/visibility.rs+60uninstall.shBRIDGE_VERSION+1−1@@ -1 +1 @@-0.10.7+0.10.8
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.10.7"+version = "0.10.8" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.7"+version = "0.10.8" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+23−1@@ -498,7 +498,29 @@ not. ## Roadmap -- v0.10.7 (this): **what the live run decided.** 0.10.6 was measured on+- v0.10.8 (this): **clear_part in passes.** 0.10.7's full live self-test+ completed:+ - nothing was throttled, and the launch was attributed by `ancestry`;+ - it created 15 documents instead of more than 32;+ - SOLIDWORKS went from 507 to 2361 MB;+ - 40 passed, 3 failed, 1 skipped.++ The 3 failures were one bug. `clear_part`'s single `DeleteSelection2`+ returned true and left a top-level sketch that an earlier check had left+ unconsumed (a cut that removed nothing keeps its sketch). Now:+ - `clear_part` walks the tree again after each delete and deletes what is+ left. It selects a feature by `IFeature::Select2`, or by name with its own+ type (a sketch as SKETCH) when that refuses. It makes up to 5 passes,+ succeeds only on an empty tree, and reports `passes` and `deleted`. The+ simulator and the real host share the loop.+ - If the part still will not clear, the self-test runs that check in a fresh+ part and records `reuseFallback` on the row. It counts the part in+ `documentsCreated`, so a check fails only on its own geometry.+ - The simulator now models the leftover sketch.+ - Docs record this run, and that the closing respawn freed 0 MB this time.++ 631 tests.+- v0.10.7: **what the live run decided.** 0.10.6 was measured on SOLIDWORKS 2026: - Hidden documents cannot be modelled. SOLIDWORKS selects only in the active document, and a hidden one cannot be activated.
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.10.7",+ "version": "0.10.8", "description": "Bridge for SOLIDWORKS desktop CAD — create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.", "author": "Adom Inc.", "license": "MIT",
docs/LIVE-RESULTS.md+3@@ -87,6 +87,9 @@ and projects the origin onto the face, so their circles were moved to sketch | A part created hidden (`visible:false`), then modelled | refused by SOLIDWORKS: a hidden document cannot be activated, and modelling selects | | `swx_com_respawn` | about 210 MB freed (1697 to 1487 MB), handles unchanged (4022 to 4056): the bridge does not pin documents | | Creating the reuse part (once) | +1.95 GB while open (1487 to 3437 MB), +285 MB after its close — observed, unexplained |+| `swx_selftest {"level":"write"}`, bridge 0.10.7 (one reused part) | complete, 475 s: 40 passed, 3 failed, 1 skipped; 15 documents created (more than 32 before); SOLIDWORKS 507 to 2361 MB (peak 3922 MB with parts open), handles 2809 to 5122; commit stayed "ok" (61.7 % to 70.0 %); no throttling, nothing recycled; launch attributed by `ancestry` |+| The self-test's closing `swx_com_respawn` | freed 0 MB (2361 to 2361 MB) — with the 210 MB above: not a saving to rely on |+| The 3 failures of that run | one bug: a single `DeleteSelection2` in `clear_part` returned true and left a top-level sketch (Sketch13, Sketch4, Sketch9) that an earlier check left unconsumed. 0.10.8 repeats the delete until the part is empty, and falls back to a fresh part for that check if it still cannot | `clear_part` (the reuse step) is marked measured on 2026 in the variants table.
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.10.7",+ "version": "0.10.8", "type": "app", "description": "Skills for your container so your AI knows how to drive the SOLIDWORKS bridge (swx_* verbs). The bridge runtime itself is the release zip — a single native swx-bridge.exe; Adom Bridge loads that.", "tags": [
skills/solidworks-memory/SKILL.md+19−9@@ -51,9 +51,15 @@ Parent skill: **solidworks-bridge** (`document_hidden`) and makes everything visible by default. - [measured] **The bridge does not pin the documents.** `swx_com_respawn` (it restarts only the bridge's COM host, releasing every reference it holds, and never calls- SOLIDWORKS) freed about 210 MB of private bytes (1697 to 1487 MB). The handle count did- not drop (4022 to 4056). So the per-document cost is SOLIDWORKS's own, kept until- SOLIDWORKS restarts.+ SOLIDWORKS) did not lower the handle count (4022 to 4056). So the per-document cost is+ SOLIDWORKS's own, kept until SOLIDWORKS restarts.+- [measured] **A respawn is not a saving to rely on.** It freed 210 MB once (1697 to+ 1487 MB) and 0 MB once (2361 to 2361 MB, at the end of a full self-test). The self-test+ still ends with one, as hygiene, and reports what it freed.+- [measured] **A full self-test with one reused document** (bridge 0.10.7, 2026-10-14):+ 15 documents instead of more than 32. SOLIDWORKS went from 507 MB to 2361 MB (a peak of+ 3922 MB while checks had parts open), handles from 2809 to 5122. Commit stayed "ok"+ (61.7 % to 70.0 %), nothing was throttled, and nothing was recycled. - [measured, unexplained] Creating the reuse part once raised SOLIDWORKS's private bytes from 1487 to 3437 MB (+1.95 GB) while it was open. After it was closed, SOLIDWORKS held 1772 MB, 285 MB more than before it was created. This happened once and has no@@ -69,10 +75,11 @@ Parent skill: **solidworks-bridge** user what holds the memory. HIGH pressure held by other applications is not a reason to stop a run that reuses one document: it keeps SOLIDWORKS flat (measured). 2. **Recycle a session the bridge started, never one the user is working in.**- [measured] On the 3DEXPERIENCE edition the launcher chain re-parents, so 0.10.6 never- recognised its own launch (`attributedSolidworks` was null). [unmeasured] From 0.10.7- the bridge claims the one sldworks.exe that appeared in the launch window- (`swx_status.launch.attributedSolidworks.how`). When SOLIDWORKS's OWN commit passes a+ [measured] On the 3DEXPERIENCE edition 0.10.6 never recognised its own launch+ (`attributedSolidworks` was null). [measured, 2026-10-14] From 0.10.7 it does:+ `swx_status.launch.attributedSolidworks.how` read `"ancestry"` after a bridge launch;+ the one new sldworks.exe of the launch window is the fallback when ancestry cannot+ reach it. When SOLIDWORKS's OWN commit passes a budget (6 GB is a sensible default on a 32 GB PC), or commit is critical and SOLIDWORKS holds a real share of it, and the session was started by the bridge and has **zero documents open**: `swx_stop {"userAuthorized": true, "reason": ...}`@@ -93,8 +100,11 @@ Parent skill: **solidworks-bridge** document cost nothing (measured). Make ONE visible part with `swx_new_document`, then run each script with `"reuseDocument": "<its path>"`. Its leading `new_part` becomes `clear_part`, which deletes every feature after the origin, and only in a part the- bridge created. `swx_selftest {"level":"write"}` does this itself (it reports- `documentsCreated`), and it ends by respawning the bridge's COM host+ bridge created. `clear_part` repeats its delete until the part is empty (0.10.8:+ measured, one pass left a sketch an earlier failed step had not used up). If the part+ still will not clear, the self-test runs that check in a fresh part instead of failing+ it, and the row says so (`reuseFallback`). `swx_selftest {"level":"write"}` does all+ this itself, reports `documentsCreated`, and ends by respawning the bridge's COM host (`comRespawn.freedMb`). [unmeasured] Between checks, the self-test recycles the bridge's own empty session only when SOLIDWORKS ITSELF is over its budget (the recycled list says when). It stops only when commit is critical, or when SOLIDWORKS is
src/comhost/com.rs+52−26@@ -5574,31 +5574,56 @@ impl ComBackend { )); } self.ensure_active(&doc, st)?;- let after_origin = Self::features_after_origin(&doc)?;- let names: Vec<String> = after_origin.iter().filter_map(|f| f.string("Name", &[])).collect();- let mut deleted = 0usize;- if !after_origin.is_empty() {- let _ = Self::clear_selection(&doc);- for f in &after_origin {- let _ = f.call("Select2", &[&Var::boolean(true), &Var::i4(0)]);+ // IN PASSES (0.10.8). Measured: one DeleteSelection2 returned true+ // and left a top-level sketch (one a previous script left+ // unconsumed). Each pass selects what is LEFT — a feature by+ // IFeature::Select2, and if that refuses, by name with its own+ // selection type (a sketch as "SKETCH") — and deletes it with+ // swDeleteSelectionOptions_e swDelete_Children 1 | swDelete_Absorbed 2+ // (help.solidworks.com, swconst; DeleteSelection2, 2006 SP1, "does+ // not ask the user to confirm the deletion").+ let ext = doc.obj("Extension", &[])?.ok_or_else(|| HostError::msg("no ModelDocExtension"))?;+ let mut last_ok = true;+ let cleared = crate::modeling::clear_in_passes(+ || Ok::<_, HostError>(Self::features_after_origin(&doc)?.iter().filter_map(|f| f.string("Name", &[])).collect()),+ |names, _pass| {+ let _ = Self::clear_selection(&doc);+ for f in Self::features_after_origin(&doc)? {+ let Some(name) = f.string("Name", &[]) else { continue };+ if !names.contains(&name) {+ continue;+ }+ let selected = f.call("Select2", &[&Var::boolean(true), &Var::i4(0)]).ok().and_then(|v| v.as_bool()) == Some(true);+ if !selected {+ let kind = match f.string("GetTypeName2", &[]).as_deref() {+ Some("ProfileFeature") => "SKETCH",+ Some("3DProfileFeature") => "3DSKETCH",+ Some("RefPlane") => "PLANE",+ Some("RefAxis") => "AXIS",+ _ => "BODYFEATURE",+ };+ let _ = Self::select_named(&doc, &name, kind, true, 0);+ }+ }+ last_ok = ext.call("DeleteSelection2", &[&Var::i4(1 | 2)])?.as_bool().unwrap_or(false);+ let _ = Self::clear_selection(&doc);+ Ok(())+ },+ 5,+ );+ let cleared = match cleared {+ Ok(c) => c,+ Err(crate::modeling::ClearError::Host(e)) => return Err(e),+ Err(crate::modeling::ClearError::Stuck { passes, left, .. }) => {+ return Err(HostError::coded(+ "clear_part_incomplete",+ format!(+ "clear_part could not delete every feature after {passes} passes (DeleteSelection2 last returned {last_ok}; still there: {})",+ left.join(", ")+ ),+ )) }- // swDeleteSelectionOptions_e: swDelete_Children 1 | swDelete_Absorbed 2- // (help.solidworks.com, swconst) — the sketches a feature- // absorbed, and anything built on it, go too. DeleteSelection2- // (2006 SP1) "does not ask the user to confirm the deletion".- let ext = doc.obj("Extension", &[])?.ok_or_else(|| HostError::msg("no ModelDocExtension"))?;- let ok = ext.call("DeleteSelection2", &[&Var::i4(1 | 2)])?.as_bool().unwrap_or(false);- let _ = Self::clear_selection(&doc);- let left = Self::features_after_origin(&doc)?;- if !ok || !left.is_empty() {- let still: Vec<String> = left.iter().filter_map(|f| f.string("Name", &[])).collect();- return Err(HostError::msg(format!(- "clear_part could not delete every feature (DeleteSelection2 returned {ok}; still there: {})",- still.join(", ")- )));- }- deleted = names.len();- }+ }; let bodies = doc.call("GetBodies2", &[&Var::i4(0), &Var::boolean(false)]).map(|b| b.as_array().len()).unwrap_or(0); // The script's own memory of planes, axes and sketches is about // features that no longer exist.@@ -5607,8 +5632,9 @@ impl ComBackend { *st = ScriptState::default(); st.restore_active = keep_active; st.created_here = created_here;- Ok(json!({"cleared": title, "featuresDeleted": deleted, "deleted": names, "bodiesLeft": bodies,- "route": "DeleteSelection2(swDelete_Children | swDelete_Absorbed)"}))+ Ok(json!({"cleared": title, "featuresDeleted": cleared.deleted.len(), "deleted": cleared.deleted,+ "passes": cleared.passes, "bodiesLeft": bodies,+ "route": "DeleteSelection2(swDelete_Children | swDelete_Absorbed), repeated until the tree is empty"})) } Op::Sketch { plane, entities, .. } => {
src/comhost/mock.rs+60−6@@ -3939,10 +3939,27 @@ impl MockBackend { if d.doc_type != "part" { return Err(HostError::msg(format!("clear_part works on a part; '{}' is not one", d.title))); }- let mut model: crate::comhost::mock_model::Model =+ let model: crate::comhost::mock_model::Model = serde_json::from_value(Value::Object(d.model.clone())).unwrap_or_default();- let deleted: Vec<String> = model.features.iter().map(|f| f.name.clone()).collect();- model.clear();+ // THE SAME PASSES the real backend runs. As measured, the+ // FIRST delete takes the features and leaves an+ // unconsumed sketch; the next pass, selecting it as a+ // SKETCH, takes it (`mock-clear-part-stuck`: never).+ let model_cell = std::cell::RefCell::new(model);+ let cleared = crate::modeling::clear_in_passes(+ || Ok::<_, HostError>(model_cell.borrow().tree_after_origin()),+ |_names, pass| {+ let mut m = model_cell.borrow_mut();+ if pass == 1 {+ m.clear();+ } else if !fault("mock-clear-part-stuck") {+ m.loose_sketches.clear();+ }+ Ok(())+ },+ 5,+ );+ let model = model_cell.into_inner(); d.bodies = model.bodies_json(); d.dimensions = Map::new(); if let Value::Object(m) = serde_json::to_value(&model).unwrap_or(Value::Null) {@@ -3950,8 +3967,21 @@ impl MockBackend { } d.dirty = true; run = fresh_run();- json!({"cleared": d.title, "featuresDeleted": deleted.len(), "deleted": deleted, "bodiesLeft": 0,- "route": "DeleteSelection2(swDelete_Children | swDelete_Absorbed)"})+ match cleared {+ Ok(c) => json!({"cleared": d.title, "featuresDeleted": c.deleted.len(), "deleted": c.deleted,+ "passes": c.passes, "bodiesLeft": 0,+ "route": "DeleteSelection2(swDelete_Children | swDelete_Absorbed), repeated until the tree is empty"}),+ Err(crate::modeling::ClearError::Stuck { passes, left, .. }) => {+ return Err(HostError::coded(+ "clear_part_incomplete",+ format!(+ "clear_part could not delete every feature after {passes} passes (DeleteSelection2 last returned true; still there: {})",+ left.join(", ")+ ),+ ))+ }+ Err(crate::modeling::ClearError::Host(e)) => return Err(e),+ } } Op::SetProp { name, value } => { let path = self.resolve(&target)?;@@ -3997,7 +4027,15 @@ impl MockBackend { let d = self.docs.iter_mut().find(|d| d.path == path).ok_or_else(|| HostError::msg("document vanished"))?; let mut model: crate::comhost::mock_model::Model = serde_json::from_value(Value::Object(d.model.clone())).unwrap_or_default();- let r = crate::comhost::mock_model::apply(&mut model, &mut run, op, active).map_err(HostError::msg)?;+ let applied = crate::comhost::mock_model::apply(&mut model, &mut run, op, active);+ // What a FAILED step left in the tree (an unconsumed+ // sketch) stays there, as it does in SOLIDWORKS.+ if applied.is_err() {+ if let Value::Object(m) = serde_json::to_value(&model).unwrap_or(Value::Null) {+ d.model = m;+ }+ }+ let r = applied.map_err(HostError::msg)?; if let Op::SetMaterial { name, .. } = op { d.material = Some(name.clone()); }@@ -4028,6 +4066,22 @@ impl MockBackend { } } }+ // A SKETCH DRAWN AND NEVER CONSUMED stays in the tree, unabsorbed+ // (0.10.8) — the shape that survived a clear_part live.+ if let Some(p) = run.pending.take() {+ if let Ok(path) = self.resolve(&pin_arg(&pinned)) {+ if let Some(d) = self.docs.iter_mut().find(|d| d.path == path) {+ let mut model: crate::comhost::mock_model::Model =+ serde_json::from_value(Value::Object(d.model.clone())).unwrap_or_default();+ if !model.loose_sketches.contains(&p.name) {+ model.loose_sketches.push(p.name.clone());+ }+ if let Value::Object(m) = serde_json::to_value(&model).unwrap_or(Value::Null) {+ d.model = m;+ }+ }+ }+ } // Put the user's active document back. if let Some(prev) = was_active { if self.find(&prev).is_some() && pinned.is_some() && !created.is_object() {
src/comhost/mock_model.rs+19−1@@ -40,6 +40,12 @@ pub struct Model { /// Face-level appearances: (a point on the face, rgb). pub face_appearances: Vec<(V3, [i64; 3])>, pub material: Option<String>,+ /// SKETCHES LEFT IN THE TREE UNCONSUMED (0.10.8): a feature that failed+ /// (T07's cut that removed nothing) or a script that ended on a sketch+ /// leaves its sketch behind, unabsorbed. MEASURED: the first+ /// DeleteSelection2 of a clear_part left exactly such a sketch.+ #[serde(default)]+ pub loose_sketches: Vec<String>, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]@@ -789,6 +795,13 @@ impl Model { self.face_appearances.clear(); } + /// The top-level tree after the origin, as clear_part walks it.+ pub fn tree_after_origin(&self) -> Vec<String> {+ let mut t: Vec<String> = self.features.iter().map(|f| f.name.clone()).collect();+ t.extend(self.loose_sketches.iter().cloned());+ t+ }+ pub fn bodies_json(&self) -> Vec<Value> { self.bodies .iter()@@ -860,7 +873,12 @@ pub fn apply(model: &mut Model, run: &mut Run, op: &Op, active: bool) -> Result< return Err(format!("the sketch '{}' was no longer selected — SOLIDWORKS returned no feature", p.name)); } run.sketch_selected = false;- let mut r = model.extrude(&p, ext, matches!(op, Op::Cut(_)))?;+ let made = model.extrude(&p, ext, matches!(op, Op::Cut(_)));+ if made.is_err() {+ // A feature that came back null leaves its sketch in the tree.+ model.loose_sketches.push(p.name.clone());+ }+ let mut r = made?; r["sketch"] = json!(p.name); r }
src/modeling.rs+83@@ -526,6 +526,51 @@ impl Selector for ProfileSelector { } } +/// What clear_part did (0.10.8).+#[derive(Clone, Debug, PartialEq)]+pub struct Cleared {+ pub passes: usize,+ pub deleted: Vec<String>,+}++/// clear_part could not empty the part.+#[derive(Clone, Debug, PartialEq)]+pub enum ClearError<E> {+ /// Something is still there after `passes` passes.+ Stuck { passes: usize, left: Vec<String>, deleted: Vec<String> },+ Host(E),+}++/// CLEAR_PART IN PASSES (0.10.8), shared by the COM host and the simulator.+///+/// MEASURED 2026-10-14 (0.10.7's self-test on SOLIDWORKS 2026): three checks+/// failed because one DeleteSelection2 over every feature after the origin+/// returned TRUE and left a top-level SKETCH behind (Sketch13, Sketch4,+/// Sketch9) — a sketch the previous check left unconsumed. So: walk, delete,+/// walk again, and repeat on whatever is left, up to `max_passes`. It succeeds+/// ONLY when the walk comes back empty. `deleted` is what each pass actually+/// removed (present before it, gone after it).+pub fn clear_in_passes<E>(+ mut remaining: impl FnMut() -> Result<Vec<String>, E>,+ mut delete: impl FnMut(&[String], usize) -> Result<(), E>,+ max_passes: usize,+) -> Result<Cleared, ClearError<E>> {+ let mut left = remaining().map_err(ClearError::Host)?;+ let mut deleted: Vec<String> = Vec::new();+ let mut passes = 0;+ while !left.is_empty() {+ if passes >= max_passes {+ return Err(ClearError::Stuck { passes, left, deleted });+ }+ passes += 1;+ delete(&left, passes).map_err(ClearError::Host)?;+ let after = remaining().map_err(ClearError::Host)?;+ deleted.extend(left.iter().filter(|n| !after.contains(n)).cloned());+ left = after;+ }+ Ok(Cleared { passes, deleted })+}+ /// Read the interop probe's output (`guid=…`, `itype=…`, `tlflags=…`) into /// the IID and whether the interface can be called through IDispatch: /// InterfaceType Dual (0) or IDispatch (2), or TypeLibType FDual (0x40) /@@ -735,6 +780,44 @@ mod tests { assert!(edge_ray([0.0; 3], &[[0.0, 0.0, 3.0], [0.0; 3]]).is_some()); } + #[test]+ fn clear_part_repeats_until_the_tree_is_empty() {+ use std::cell::RefCell;+ // THE MEASURED CASE: the first delete takes the features and leaves a sketch.+ let tree = RefCell::new(vec!["Boss-Extrude1".to_string(), "Cut-Extrude1".to_string(), "Sketch13".to_string()]);+ let r = clear_in_passes(+ || Ok::<_, String>(tree.borrow().clone()),+ |names: &[String], pass| {+ let mut t = tree.borrow_mut();+ if pass == 1 {+ t.retain(|n| n.starts_with("Sketch") && names.contains(n));+ } else {+ t.clear();+ }+ Ok(())+ },+ 5,+ );+ assert_eq!(r, Ok(Cleared { passes: 2, deleted: vec!["Boss-Extrude1".into(), "Cut-Extrude1".into(), "Sketch13".into()] }));+ // Nothing there: no pass at all.+ assert_eq!(clear_in_passes(|| Ok::<_, String>(vec![]), |_: &[String], _| Ok(()), 5), Ok(Cleared { passes: 0, deleted: vec![] }));+ // Something that never goes: stuck after exactly max passes, and SAID.+ let calls = RefCell::new(0);+ let r = clear_in_passes(+ || Ok::<_, String>(vec!["Sketch9".to_string()]),+ |_: &[String], _| {+ *calls.borrow_mut() += 1;+ Ok(())+ },+ 5,+ );+ assert_eq!(r, Err(ClearError::Stuck { passes: 5, left: vec!["Sketch9".into()], deleted: vec![] }));+ assert_eq!(*calls.borrow(), 5);+ // A host failure stops it, as a host failure.+ let r = clear_in_passes(|| Ok::<_, String>(vec!["A".into()]), |_: &[String], _| Err("COM said no".to_string()), 5);+ assert_eq!(r, Err(ClearError::Host("COM said no".to_string())));+ }+ #[test] fn the_iid_probe_is_read_strictly() { let g = "guid=4c1e9cc6-3d6d-11d5-9a8c-00c04f80e3e7\nitype=0\n";
src/selftest.rs+47−13@@ -203,10 +203,11 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { } } let target = if reuse { shared.clone() } else { None };- if target.is_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 = geometry_check(ctx, &mut run, &check, memory_override.as_deref(), target.as_deref());+ 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);@@ -286,8 +287,8 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { 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 of SOLIDWORKS's memory and never- // touched SOLIDWORKS itself.+ // 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)); return finish_with(level, &sw, ready, true, started, run, None, memory_log.to_json(memory_override.as_deref())); }@@ -436,7 +437,7 @@ fn respawn_com_host(ctx: &Ctx) -> Value { let r = crate::verbs::dispatch( ctx, "swx_com_respawn",- &json!({"reason": "swx_selftest: release the COM references the run held (measured: about 210 MB)"}),+ &json!({"reason": "swx_selftest: release the COM references the run held"}), ); let after = crate::memory::sample(crate::launchos::os().as_ref()); json!({@@ -838,20 +839,52 @@ fn titles(ctx: &Ctx) -> Vec<String> { } /// One check: build, MEASURE, compare, close what it made.-fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<&str>, reuse: Option<&str>) -> bool {+/// 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 mut sargs = match reuse {+ 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}),- };- if let Some(r) = memory_override {- sargs["ignoreMemoryPressure"] = json!(true);- sargs["reason"] = json!(r);+ });+ 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 r = crate::verbs::dispatch(ctx, "swx_run_script", &sargs); 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); let mut fails: Vec<String> = Vec::new();@@ -1016,6 +1049,7 @@ fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<& "expected": expected, "measured": measured, "fails": fails,+ "reuseFallback": fallback, }));- reuse.is_some() && r.get("error").and_then(|e| e.as_str()).map(|e| e.contains("(clear_part) failed")).unwrap_or(false)+ CheckDocs { clear_failed: fallback.is_some(), fresh: reuse.is_none() || fallback.is_some() } }
tests/visibility.rs+60@@ -435,3 +435,63 @@ fn high_pressure_held_by_other_applications_does_not_stop_the_selftest() { ); check!(b, "nothing was quit or terminated", !machine.join("quit.jsonl").exists() && !machine.join("terminated.jsonl").exists(), "quit / terminated"); }++/// 0.10.8 — MEASURED on 0.10.7's live self-test: three checks failed because+/// clear_part's one DeleteSelection2 returned true and left a top-level sketch+/// (Sketch13, Sketch4, Sketch9) — one a previous check left unconsumed (a cut+/// that removed nothing keeps its sketch). clear_part now repeats until the+/// tree is empty, and reports its passes.+#[test]+fn clear_part_takes_a_second_pass_for_a_sketch_left_behind() {+ let (b, _m) = boot("vis-clear-passes", &[]);+ b.call("swx_start", json!({}));+ let made = b.call("swx_new_document", json!({"type": "part"}));+ let p = made["documentIdentity"]["path"].as_str().unwrap_or("").to_string();+ // A cut from Top Plane with the body on +Y removes nothing: the step fails+ // and its sketch stays in the tree, unconsumed.+ let failed = b.call("swx_run_script", json!({"reuseDocument": p, "script": [+ {"op": "new_part"},+ {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},+ {"op": "extrude", "depthMm": 10},+ {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 10, "heightMm": 10, "anchor": "center"},+ {"op": "cut", "depthMm": 5}]}));+ let r = b.call("swx_run_script", json!({"reuseDocument": p, "script": box_script(json!({"op": "new_part"}))}));+ let ib = b.call("swx_inspect_bodies", json!({"path": p}));+ let deleted = r["steps"][0]["result"]["deleted"].as_array().cloned().unwrap_or_default();+ check!(+ b,+ "the next clear_part took TWO passes — the features, then the sketch left behind — and the part holds only the new box",+ failed["success"] == false && r["success"] == true && r["steps"][0]["result"]["passes"] == 2+ && deleted.iter().any(|d| d.as_str().unwrap_or("").starts_with("Sketch"))+ && ib["bodyCount"] == 1+ && ib["bodies"][0]["volumeMm3"].as_f64().map(|v| (v - 4000.0).abs() < 1e-6) == Some(true),+ format!("{}\n{}", s(&r["steps"][0]), s(&ib))+ );+}++/// When the shared part STILL cannot be cleared, the self-test does not fail+/// the check for it: the check runs in a fresh part, the row says why, and the+/// fresh part is counted. A check fails only on its own geometry.+#[test]+fn a_part_that_will_not_clear_falls_back_to_a_fresh_part_in_the_selftest() {+ let (b, _m) = boot("vis-clear-fallback", &[]);+ b.call("swx_start", json!({}));+ std::fs::write(b.scratch.join("mock-clear-part-stuck"), "").unwrap();+ let r = b.call("swx_selftest", json!({"level": "write", "only": ["T07", "T08"]}));+ let rows = r["results"].as_array().cloned().unwrap_or_default();+ let row = |id: &str| rows.iter().find(|x| x["name"].as_str().unwrap_or("").starts_with(id)).cloned().unwrap_or(Value::Null);+ check!(+ b,+ "T08 could not reuse the part T07 left a sketch in: it ran in a FRESH part, passed, and the row says what was left",+ r["summary"]["failed"] == 0 && row("T07")["pass"] == true && row("T08")["pass"] == true+ && row("T08")["reuseFallback"].as_str().unwrap_or("").starts_with("fresh part — clear_part left Sketch")+ && row("T07")["reuseFallback"].is_null(),+ s(&json!(rows))+ );+ check!(+ b,+ "documentsCreated counts it: the shared part, the fallback part, the scratch part",+ r["documentsCreated"] == 3,+ s(&r["documentsCreated"])+ );+}
uninstall.sh