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.10 source
23 files changed
+743−125
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+36−1SKILL.md+12−5bridge.json+3−2docs/COMPATIBILITY.md+17−8docs/LIVE-RESULTS.md+32examples/lego-2x4.json+2−2install.shpackage.json+1−1references/modelling-and-export.md+7−1src/comhost/com.rs+11−6src/comhost/mock.rs+18−10src/compat.rs+80−50src/selftest.rs+162−9src/verbs.rs+54−6src/verbs3.rs+13−5tests/hidefileext.rs+124tests/issue1.rs+20−12tools/cswa/SOURCES.md+139tools/dsl-live-check.py+9−4uninstall.shBRIDGE_VERSION+1−1@@ -1 +1 @@-0.10.9+0.10.10
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.10.9"+version = "0.10.10" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.9"+version = "0.10.10" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+36−1@@ -498,7 +498,42 @@ not. ## Roadmap -- v0.10.9 (this): **the presence rule, made symmetric.** Measured+- v0.10.10 (this): **SOLIDWORKS 2021, tested — and the one bug it found.**+ Michael ran 0.10.9's `swx_selftest {"level":"write"}` on SOLIDWORKS 2021+ (COM 29.5.1, Windows 11): 44 passed, 6 failed. Every modelling check passed,+ and the LEGO brick came out identical to 2026 (4 features, 2073.362 mm³, 39+ faces). The 6 failures were ONE bug: the self-test addressed its scratch part+ as `Part14-….SLDPRT`, and with Windows' "Hide extensions for known file types"+ on (the Windows default) SOLIDWORKS titles it `Part14-…`. The close missed+ too and left the part open.++ Now:+ - **Titles match extension-insensitively everywhere** (`same_title`: a+ trailing `.sldprt/.sldasm/.slddrw`, any case, is ignored on both sides; a+ dot elsewhere, `Rev1.2`, is part of the name). This covers document+ resolution, ownership of unsaved documents, closing, activation and the+ collision checks, in the server and in the COM host.+ - **The self-test addresses its own documents by path**, retries a missed+ close by title, and reports `documentsLeftOpen` instead of leaving a part+ silently.+ - **`swx_new_document` reports the title SOLIDWORKS shows**, read back rather+ than built from the file name, alongside the path.+ - **A cut-off self-test leaves nothing behind.** Every document it creates is+ recorded and struck off when closed. The next `swx_selftest` closes what a+ cut-off run left. `swx_status.orphanedSelftestDocuments` reports them, and+ closes nothing (a run may be in progress). Two self-tests cannot run at+ once (`selftest_running`).+ - `swx_selftest` replies (and its catalog entry) lead with `_timeoutHint`:+ call it with `responseMode:"stream"` or `"job"`.+ - **2021 is tested.** `compat::OP_FACTS` carries the community measurement+ (`by:"community"`) for the 32 variants the run measured, so+ `proven.live` is true on 2021 for those. The T33+ checks have run on 2026+ only. The ownership row is numbered T23.+ - The simulator can hide extensions (`mock-hide-file-ext`). The full+ self-test and the six verbs pass both ways.++ 639 tests.+- v0.10.9: **the presence rule, made symmetric.** Measured 2026-10-14 on hosted 0.10.7, with Drew at the keyboard during a battery: - 17 `swx_reset_workspace` calls in a row were refused for his presence; - 18 new scratch parts were allowed and piled up on his screen;
SKILL.md+12−5@@ -82,9 +82,13 @@ BEFORE you do it: password, and never retries a rejected one. The measured login flow is in the **solidworks-sign-in** skill. Never launch `sldworks.exe` or the launcher any other way.-7. **Name the document you mean.** Pass `targetDocument` (a title) or `path` on- every verb that changes something; the active document follows window focus,- and the user may click something else mid-run.+7. **Name the document you mean — by `path` when it has one.** Pass `path` (or+ `targetDocument`, a title) on every verb that changes something; the active+ document follows window focus, and the user may click something else+ mid-run. A TITLE depends on Windows: with "Hide extensions for known file+ types" on (the default) SOLIDWORKS titles a part `Part14-17`, otherwise+ `Part14-17.SLDPRT`. The bridge matches either spelling (0.10.10), but a path+ is an identity and a title is not. `swx_new_document` returns both. 8. **`swx_stop` quits only a session with zero documents open**, and `force` never waives that. Treat a quit as close to a one-way door: relaunching takes minutes and can stop at the sign-in window again.@@ -121,8 +125,11 @@ BEFORE you do it: There are also `fillet`, `chamfer`, `shell`, `linear_pattern`, `circular_pattern` and `set_appearance`. A cut goes along its plane's −normal. A worked LEGO brick in 4 features is in `examples/lego-2x4.json`.- SOLIDWORKS 2021 and later are supported (only 2026 is tested); on any- install, `swx_selftest {"level":"write"}` returns one JSON of what works.+ SOLIDWORKS 2021 and later are supported; 2026 is tested by us and 2021 by a+ community run (identical LEGO numbers). On any install,+ `swx_selftest {"level":"write"}` returns one JSON of what works — call it+ with `responseMode:"stream"` or `"job"`: it runs 5-8 minutes, and a default+ 120 s client budget cuts it off (the next run closes what it left open). 6. `swx_state {}` any time — open documents, the active one, the dialog census and a background screenshot, in one call. 7. Done: `swx_reset_workspace` closes only what the bridge created and has not
bridge.json+3−2@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.10.9",+ "version": "0.10.10", "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",@@ -185,7 +185,8 @@ "compatibility": { "solidworks": { "supported": ">=2021",- "tested": ["2026 (3DEXPERIENCE SOLIDWORKS, Makers)"],+ "tested": ["2026 (3DEXPERIENCE SOLIDWORKS, Makers)",+ "2021 (classic desktop: a community swx_selftest run, 2026-10-15, bridge 0.10.9)"], "basis": "every COM member the bridge calls is available in SOLIDWORKS 2017 or earlier (help.solidworks.com Availability; docs/COMPATIBILITY.md); swx_selftest reports per-verb results on any install" } },
docs/COMPATIBILITY.md+17−8@@ -23,21 +23,30 @@ only ever **run** against **3DEXPERIENCE SOLIDWORKS 2026** (Makers, revision checks. `swx_selftest {"level":"write"}` passed 43, failed 0 and skipped 1. The LEGO brick matched Michael's 2021 numbers, face count included. See [LIVE-RESULTS.md](LIVE-RESULTS.md).-- **[unmeasured] 2021 is still waiting for a `swx_selftest` run.** Michael's- reports show the bridge installing and working on 2021. That is evidence, not- a test.+- **[measured, community] Tested on SOLIDWORKS 2021.** On 2026-10-15 Michael ran+ `swx_selftest {"level":"write"}` with bridge 0.10.9 on SOLIDWORKS 2021 (COM+ 29.5.1, classic desktop, Windows 11): every modelling check passed (T01-T22,+ T24-T32), and the LEGO brick came out identical to the 2026 numbers. The 6+ failures were ONE bug, the only 2021 finding: the self-test addressed its+ scratch part by a title WITH the extension, and with Windows' "Hide extensions+ for known file types" ON (the Windows default) SOLIDWORKS titles it without+ one. Fixed in 0.10.10: titles compare extension-insensitively everywhere, and+ the bridge addresses its own documents by path. See+ [LIVE-RESULTS.md](LIVE-RESULTS.md#solidworks-2021-community). - Each `swx_run_script` step reports `proven: {live, measuredOn}`. `live` is true only when every variant the step ran was measured on the release it ran- on, so on 2021 it is `false` for now. The variants and their measurements are- one table, `compat::OP_FACTS`. `swx_describe` and `swx_selftest` report the- same table as `liveMeasured`.+ on. On 2021 that is the 32 variants the community run measured; the T33++ checks (0.10.4) have run on 2026 only. The variants and their measurements are+ one table, `compat::OP_FACTS`; each measurement says `by` (`adom` or+ `community`). `swx_describe` and `swx_selftest` report the same table as+ `liveMeasured`. - `bridge.json` declares `"solidworks": ">=2021"`. It said `>=2024`, which was untrue in both directions: it never stopped Michael installing, and nothing in the API needs 2024. - `swx_status.solidworks` reports the version it reads at attach: - `majorVersion`, for example 29; - `versionSupport`, which holds `{revision, majorVersion, year, supported,- tested, supportedFrom: 2021, testedOn: [2026], lacks}`. `supported` and+ tested, supportedFrom: 2021, testedOn: [2026, 2021], lacks}`. `supported` and `tested` are separate claims. ## What breaks on 2021, and what the bridge does@@ -51,7 +60,7 @@ only ever **run** against **3DEXPERIENCE SOLIDWORKS 2026** (Makers, revision ## Classic desktop vs 3DEXPERIENCE (Connected) -| | 3DEXPERIENCE SOLIDWORKS (2026, measured) | Classic desktop (2021, unmeasured) |+| | 3DEXPERIENCE SOLIDWORKS (2026, measured) | Classic desktop (2021: a community self-test run, 2026-10-15; rows marked [unmeasured] still are) | |---|---|---| | How it is detected | An install path or registry entry naming 3DEXPERIENCE, or the 3DEXPERIENCE Launcher | An install path under `SOLIDWORKS Corp`, OR a registry key `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS <year>` with an exe found and no 3DEXPERIENCE marker. The exe is also looked up from `...\SOLIDWORKS <year>\Setup` value `SolidWorks Folder` [unmeasured]. `swx_status.solidworks.registryYears` lists every `SOLIDWORKS <year>` key. That is a fact about the registry, not the edition: the 3DEXPERIENCE 2026 install writes `SOLIDWORKS 2026` too (measured 2026-10-13). `classicYears` lists only the years with no 3DEXPERIENCE entry or install path for the same release; on Drew's 2026 machine that is `[]`. | | `edition` / `launchRoute` | `connected` / `platformShortcut` | `desktop` / `com` |
docs/LIVE-RESULTS.md+32@@ -22,6 +22,38 @@ Built in 4 features, measured live: | Colour | [200, 0, 0] (set before the material, kept after it) | red requested | | Density / mass | 1020.0 kg/m^3 / 0.0021148 kg | ABS 1020 / 0.0021148 | +## SOLIDWORKS 2021 (community)++Run 2026-10-15 by Michael (issue #1) with bridge **0.10.9** on **SOLIDWORKS 2021**+(COM RevisionNumber 29.5.1, classic desktop, Windows 11, ab 2.1.168). His reply,+not our machine: it counts as a measurement `by:"community"` in `compat::OP_FACTS`.++- `swx_selftest {"level":"write"}`: **44 passed, 6 failed** (380 s). Every+ modelling check passed: T01-T22 and T24-T32 (T23, the ownership check, was an+ unnumbered row until 0.10.10).+- The 6 failures were ONE bug: `swx_design_info`, `swx_get_parameters`,+ `swx_set_parameter`, `swx_export_step`, `swx_save` and `swx_close_doc` aimed+ at the scratch part as `Part14-1792088642002.SLDPRT` while SOLIDWORKS titled it+ `Part14-1792088642002` — Windows' "Hide extensions for known file types"+ (HideFileExt=1, the Windows default) is on there and off on our laptop. The+ close missed too, so the part was left open; closing it by path worked.+ Fixed in 0.10.10: title matching ignores `.sldprt/.sldasm/.slddrw`, the+ self-test addresses its own documents by path, and `swx_new_document` reports+ the title SOLIDWORKS shows.+- Issue #2's fixes confirmed: `densitySource:"bodyMassProperty"`,+ `bridgeCreatedDocument:true`.++The LEGO 2x4 brick on 2021, identical to 2026:++| Measured | SOLIDWORKS 2021 (0.10.9) | SOLIDWORKS 2026 (0.10.3) |+|---|---|---|+| Features | 4 | 4 |+| Volume | 2073.362 mm^3 | 2073.362 mm^3 |+| Bounding box | [-15.9, 0.0, -7.9] to [15.9, 11.3, 7.9] mm | the same |+| Faces | 39 | 39 |+| Colour | [200, 0, 0] | [200, 0, 0] |+| Density / mass | 1020 kg/m^3 / 0.0021148 kg | 1020 kg/m^3 / 0.0021148 kg |+ ## Every check | Id | Check | Issue | Expected volume (mm^3) | Measured (mm^3) | Result |
examples/lego-2x4.json+2−2@@ -38,7 +38,7 @@ ] }, "volumeMm3": "2073.3615 = block 4823.424 - hollow 3388.056 + tubes 391.894 + studs 246.100 \u2014 the same 2073.3615208 mm\u00b3 SOLIDWORKS 2021 measured for Michael's 22-feature brick (issue #2's volumeM3 2.0733615208e-6)",- "status": "UNMEASURED in SOLIDWORKS \u2014 asserted only in the bridge's simulator (tests/issue1.rs); run it live and compare with swx_inspect_bodies"+ "status": "MEASURED on SOLIDWORKS 2026 (2026-10-14, bridge 0.10.3+) and SOLIDWORKS 2021 (2026-10-15, bridge 0.10.9, community run by Michael, issue #1): identical numbers" }, "conventions": "Top Plane: sketch +x = model +X, sketch +y = model -Z; an extrude goes +Y (the plane's normal), a cut goes -Y unless reverse:true. The brick is symmetric, so the -Z mapping does not change it. The material comes BEFORE the appearance: applying a material wipes a part's appearance (SOLIDWORKS behaviour; since 0.10.2 the bridge re-applies a colour set earlier in the same script, but this order needs nothing re-applied).", "script": [@@ -235,4 +235,4 @@ "value": "LEGO 2x4 brick" } ]-}+}
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.10.9",+ "version": "0.10.10", "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": [
references/modelling-and-export.md+7−1@@ -57,7 +57,8 @@ adom-bridge --ai-thread "<thread>" swx_save_as '{"outputPath":"C:\\parts\\bracke - **The ops (0.10.0).** Michael asked for these in issue #1. They were MEASURED live on SOLIDWORKS 2026 on 2026-10-14: `tools/dsl-live-check.py` passed 30 of 30 checks. See `docs/LIVE-RESULTS.md` in the bridge repo.- SOLIDWORKS 2021 is still waiting for a `swx_selftest {"level":"write"}` run.+ On SOLIDWORKS 2021 a community `swx_selftest {"level":"write"}` run+ (2026-10-15, bridge 0.10.9) passed every modelling check, LEGO brick identical. Each step reports `proven: {live, measuredOn}`. `live` is true only when every variant the step ran was measured on the release it ran on. When it is false, `unmeasured` names the variant, so check that step's geometry with@@ -173,6 +174,11 @@ adom-bridge --ai-thread "<thread>" swx_save_as '{"outputPath":"C:\\parts\\bracke - `swx_pack_and_go {"out":"C:\\handoff"}` — self-contained copy with all references, for sending a design anywhere. 4. `swx_state {}` any time — open docs + active doc, the look-then-act check.+5. **Address documents by `path`.** A title is what Windows' "Hide extensions+ for known file types" makes of the file name: `Part14-17` with it on (the+ Windows default, measured on a SOLIDWORKS 2021 machine), `Part14-17.SLDPRT`+ with it off. Since 0.10.10 every title match ignores `.sldprt/.sldasm/.slddrw`,+ but `swx_new_document` and `swx_open` give you the path — use it. ## Export and cloud
src/comhost/com.rs+11−6@@ -312,7 +312,8 @@ impl ComBackend { // open" — `documents()` raises docs_enumeration_failed instead. let mut hits: Vec<Disp> = Vec::new(); for doc in Self::documents(&sw)? {- if doc.string("GetTitle", &[]).as_deref() == Some(title) {+ // Extension-insensitive (0.10.10): GetTitle follows HideFileExt.+ if doc.string("GetTitle", &[]).map(|t| crate::verbs::same_title(&t, title)).unwrap_or(false) { hits.push(doc); } }@@ -668,7 +669,7 @@ impl ComBackend { .filter(|(title, path)| { let others: Vec<String> = seen .iter()- .filter(|(t, p)| t == title && !crate::verbs::same_path(p, path))+ .filter(|(t, p)| crate::verbs::same_title(t, title) && !crate::verbs::same_path(p, path)) .map(|(_, p)| p.clone()) .collect(); if others.is_empty() {@@ -2015,9 +2016,9 @@ impl ComBackend { match &path { Some(p) => self.created.retain(|(t, cp)| { !cp.as_deref().map(|q| crate::verbs::same_path(q, p)).unwrap_or(false)- && !(cp.is_none() && *t == title)+ && !(cp.is_none() && crate::verbs::same_title(t, &title)) }),- None => self.created.retain(|(t, cp)| !(cp.is_none() && *t == title)),+ None => self.created.retain(|(t, cp)| !(cp.is_none() && crate::verbs::same_title(t, &title))), } self.created.push((title, path)); crate::comhost::created_store::save_session(self.created_session, &self.created);@@ -2045,7 +2046,7 @@ impl ComBackend { .created .iter() .any(|(_, cp)| cp.as_deref().map(|q| crate::verbs::same_path(q, p)).unwrap_or(false)),- None => self.created.iter().any(|(t, cp)| cp.is_none() && *t == title),+ None => self.created.iter().any(|(t, cp)| cp.is_none() && crate::verbs::same_title(t, &title)), } } @@ -2839,6 +2840,10 @@ impl ComBackend { None }; let mut errors = 0i32;+ // THE NAME SOLIDWORKS ITSELF SHOWS (0.10.10): the caller's title is+ // matched extension-insensitively (HideFileExt decides whether GetTitle+ // carries ".SLDPRT"), and ActivateDoc3 is handed the document's own.+ let shown = self.resolve(&json!({"docName": name})).ok().and_then(|d| d.string("GetTitle", &[])).unwrap_or_else(|| name.to_string()); // ActivateDoc3(Name, UseUserPreferences, Option, byref Errors). // swRebuildOnActivation_e: 1 = swDontRebuildActiveDoc. 0 is // swUserDecision — it PROMPTS the user (help.solidworks.com swconst;@@ -2847,7 +2852,7 @@ impl ComBackend { .call( "ActivateDoc3", &[- &Var::bstr(name),+ &Var::bstr(&shown), &Var::boolean(false), &Var::i4(1), &Var::byref_i4(&mut errors as *mut i32),
src/comhost/mock.rs+18−10@@ -29,6 +29,14 @@ pub fn doc_type_of(path: &str) -> Option<&'static str> { /// Last path segment, splitting on BOTH separators: these are Windows paths /// that the mock may well be handling on Linux, where `/` alone would return /// the entire string.+/// The TITLE SOLIDWORKS shows for a file: its name, without the extension when+/// Windows hides extensions for known file types (`mock-hide-file-ext`: the+/// Windows default, and Michael's machine — measured 2026-10-15).+pub fn shown_title(path: &str) -> String {+ let name = base_name(path);+ if fault("mock-hide-file-ext") { crate::verbs::title_stem(name).to_string() } else { name.to_string() }+}+ pub fn base_name(path: &str) -> &str { path.rsplit(['/', '\\']).next().unwrap_or(path) }@@ -190,7 +198,7 @@ impl MockDoc { } fn opened(path: String, doc_type: &'static str, config: String) -> MockDoc {- let title = base_name(&path).to_string();+ let title = shown_title(&path); MockDoc { path, title,@@ -751,7 +759,7 @@ impl MockBackend { .filter(|s| !s.is_empty()) .or_else(|| args.get("name").and_then(|v| v.as_str()).filter(|s| !s.is_empty())); if let Some(title) = title {- let hits: Vec<&MockDoc> = self.docs.iter().filter(|d| d.title == title).collect();+ let hits: Vec<&MockDoc> = self.docs.iter().filter(|d| crate::verbs::same_title(&d.title, title)).collect(); return match hits.len() { 1 => Ok(hits[0].path.clone()), 0 => Err(HostError::coded(@@ -802,9 +810,9 @@ impl MockBackend { match &path { Some(p) => self.created.retain(|(t, cp)| { !cp.as_deref().map(|q| crate::verbs::same_path(q, p)).unwrap_or(false)- && !(cp.is_none() && *t == title)+ && !(cp.is_none() && crate::verbs::same_title(t, &title)) }),- None => self.created.retain(|(t, cp)| !(cp.is_none() && *t == title)),+ None => self.created.retain(|(t, cp)| !(cp.is_none() && crate::verbs::same_title(t, &title))), } self.created.push((title, path)); crate::comhost::created_store::save_session(self.created_session, &self.created);@@ -842,7 +850,7 @@ impl MockBackend { // real backend has always compared `GetPathName`; this is that rule. let reported = Self::reported_path(d); if reported.is_empty() {- self.created.iter().any(|(t, p)| p.is_none() && *t == d.title)+ self.created.iter().any(|(t, p)| p.is_none() && crate::verbs::same_title(t, &d.title)) } else { self.created .iter()@@ -1938,7 +1946,7 @@ impl MockBackend { d.dirty = false; d.temp_path = None; d.path = out.clone();- d.title = crate::comhost::mock::base_name(&out).to_string();+ d.title = shown_title(&out); } self.publish_session(); Ok(json!({@@ -2048,7 +2056,7 @@ impl MockBackend { // check, which is what `titleCollisionsCheckedOver` says. let others: Vec<String> = walked .iter()- .filter(|(t, p, _, _)| t == title && !crate::verbs::same_path(p, path))+ .filter(|(t, p, _, _)| crate::verbs::same_title(t, title) && !crate::verbs::same_path(p, path)) .map(|(_, p, _, _)| p.clone()) .collect(); if others.is_empty() {@@ -2279,7 +2287,7 @@ impl MockBackend { // rename rather than silently passing it on. let renamed = std::env::var("SWX_MOCK_RENAME_ON_EXPORT").as_deref() == Ok("1"); if renamed {- let title = base_name(out).to_string();+ let title = shown_title(out); if let Some(d) = self.docs.iter_mut().find(|d| d.path == path) { d.title = title; }@@ -2664,7 +2672,7 @@ impl MockBackend { let key = self .docs .iter()- .find(|d| d.title == name || d.path == name)+ .find(|d| crate::verbs::same_title(&d.title, name) || d.path == name) .map(|d| d.path.clone()) .ok_or_else(|| { HostError::msg(format!(@@ -3314,7 +3322,7 @@ impl MockBackend { if !copy { // A real save-as RETITLES the open document; copy mode does not. if let Some(d) = self.docs.iter_mut().find(|d| d.path == path) {- d.title = base_name(&out).to_string();+ d.title = shown_title(&out); d.saved = true; d.dirty = false; }
src/compat.rs+80−50@@ -4,13 +4,15 @@ //! Issue #1/#2's reporter (Michael) runs SOLIDWORKS 2021 (COM RevisionNumber //! 29.5.1). Every COM member this bridge calls was checked against its //! help.solidworks.com "Availability" line (docs/COMPATIBILITY.md); the-//! newest is SOLIDWORKS 2017 (Revision 25), so 2021 is SUPPORTED. Only the-//! 3DEXPERIENCE 2026 install has been TESTED.+//! newest is SOLIDWORKS 2017 (Revision 25), so 2021 is SUPPORTED. TESTED: 3DEXPERIENCE 2026 (ours) and,+//! since 2026-10-15, SOLIDWORKS 2021 (a community swx_selftest run, 0.10.9). /// The oldest SOLIDWORKS this bridge supports, by the API audit. pub const SUPPORTED_MIN_MAJOR: u32 = 29; // SOLIDWORKS 2021 /// The release the bridge has actually run against.-pub const TESTED_MAJOR: &[u32] = &[34]; // SOLIDWORKS 2026 (3DEXPERIENCE, Makers)+/// SOLIDWORKS 2026 (3DEXPERIENCE, ours) and 2021 (classic desktop: a community+/// run of swx_selftest {level:"write"}, 2026-10-15, bridge 0.10.9).+pub const TESTED_MAJOR: &[u32] = &[34, 29]; /// `RevisionNumber` "29.5.1" -> 29. pub fn major_of(revision: &str) -> Option<u32> {@@ -88,31 +90,41 @@ pub struct Measurement { pub edition: &'static str, pub date: &'static str, pub bridge: &'static str,+ /// Who ran it: "adom" (our own machine) or "community" (a user's install,+ /// their reply sent back).+ pub by: &'static str, } impl Measurement { pub fn to_json(&self) -> serde_json::Value {- serde_json::json!({"solidworks": self.year, "major": self.major, "edition": self.edition, "date": self.date, "bridge": self.bridge})+ serde_json::json!({"solidworks": self.year, "major": self.major, "edition": self.edition, "date": self.date, "bridge": self.bridge, "by": self.by}) } } /// 2026-10-14, bridge 0.10.3, SOLIDWORKS 2026 (major 34, 3DEXPERIENCE /// "Connected"): tools/dsl-live-check.py 30/30 and swx_selftest /// {level:"write"} 43 passed / 0 failed / 1 skipped (docs/LIVE-RESULTS.md).-pub const LIVE_2026_10_14: Measurement = Measurement { year: 2026, major: 34, edition: "connected", date: "2026-10-14", bridge: "0.10.3" };+pub const LIVE_2026_10_14: Measurement = Measurement { year: 2026, major: 34, edition: "connected", date: "2026-10-14", bridge: "0.10.3", by: "adom" }; /// The same day, bridge 0.10.4: eight more checks added to /// tools/dsl-live-check.py (T33-T39, T42), all passing on the same install.-pub const LIVE_2026_10_14_B: Measurement = Measurement { year: 2026, major: 34, edition: "connected", date: "2026-10-14", bridge: "0.10.4" };+pub const LIVE_2026_10_14_B: Measurement = Measurement { year: 2026, major: 34, edition: "connected", date: "2026-10-14", bridge: "0.10.4", by: "adom" }; /// The same day, bridge 0.10.6: twelve builds in ONE reused part /// (`reuseDocument`, its new_part becoming clear_part) — memory flat, and /// clear_part re-reads the tree and fails if anything is left.-pub const LIVE_2026_10_14_C: Measurement = Measurement { year: 2026, major: 34, edition: "connected", date: "2026-10-14", bridge: "0.10.6" };+pub const LIVE_2026_10_14_C: Measurement = Measurement { year: 2026, major: 34, edition: "connected", date: "2026-10-14", bridge: "0.10.6", by: "adom" }; -const M2026: &[Measurement] = &[LIVE_2026_10_14];+/// 2026-10-15, bridge 0.10.9, SOLIDWORKS 2021 (major 29, classic desktop,+/// COM 29.5.1, Windows 11): a COMMUNITY run — Michael's swx_selftest+/// {level:"write"} reply (issue #1). Every modelling check passed (T01-T22,+/// T24-T32), the LEGO brick to the same numbers as 2026; the 6 failures were+/// one bug (titles and HideFileExt, fixed in 0.10.10), in verbs, not ops.+pub const LIVE_2021_10_15: Measurement = Measurement { year: 2021, major: 29, edition: "desktop", date: "2026-10-15", bridge: "0.10.9", by: "community" };++const M2026_2021: &[Measurement] = &[LIVE_2026_10_14, LIVE_2021_10_15];+const M2026C_2021: &[Measurement] = &[LIVE_2026_10_14_C, LIVE_2021_10_15]; const M2026B: &[Measurement] = &[LIVE_2026_10_14_B];-const M2026C: &[Measurement] = &[LIVE_2026_10_14_C]; const UNMEASURED: &[Measurement] = &[]; /// One variant of one DSL op, and where it was measured live.@@ -126,63 +138,63 @@ pub struct OpFact { } pub const OP_FACTS: &[OpFact] = &[- OpFact { id: "new_part", what: "a new part document", checks: "every check", measured: M2026 },+ OpFact { id: "new_part", what: "a new part document", checks: "every check", measured: M2026_2021 }, OpFact { id: "new_assembly", what: "a new assembly document", checks: "not in the live battery", measured: UNMEASURED }, OpFact { id: "new_part.invisible", what: "a new part created HIDDEN (DocumentVisible false around NewDocument) — it CANNOT be modelled: a hidden document cannot be activated, and every modelling op selects (measured 2026-10-14)", checks: "creation worked live (0.10.6); its modelling failed, so scripts now refuse it (document_hidden)", measured: UNMEASURED }, OpFact { id: "new_assembly.invisible", what: "a new assembly created HIDDEN (cannot be modelled, as new_part.invisible)", checks: "not run live", measured: UNMEASURED },- OpFact { id: "clear_part", what: "every feature after the origin deleted from the bridge's own part (DeleteSelection2), so one document serves a whole battery", checks: "12 reused builds (reuseDocument), 2026-10-14", measured: M2026C },- OpFact { id: "sketch.plane.front", what: "a sketch on Front Plane", checks: "T01 T03 T10 T14 T27", measured: M2026 },- OpFact { id: "sketch.plane.top", what: "a sketch on Top Plane", checks: "T02 T13 T24 T30", measured: M2026 },+ OpFact { id: "clear_part", what: "every feature after the origin deleted from the bridge's own part (DeleteSelection2), so one document serves a whole battery", checks: "12 reused builds (reuseDocument), 2026-10-14", measured: M2026C_2021 },+ OpFact { id: "sketch.plane.front", what: "a sketch on Front Plane", checks: "T01 T03 T10 T14 T27", measured: M2026_2021 },+ OpFact { id: "sketch.plane.top", what: "a sketch on Top Plane", checks: "T02 T13 T24 T30", measured: M2026_2021 }, OpFact { id: "sketch.plane.right", what: "a sketch on Right Plane (sketch +x = model −Z, +y = model +Y; a boss goes +X)", checks: "T35", measured: M2026B }, OpFact { id: "sketch.plane.named", what: "a sketch on a named plane other than Front/Top/Right", checks: "not in the live battery", measured: UNMEASURED },- OpFact { id: "sketch.offset.top.positive", what: "a sketch on a plane offset + from Top Plane", checks: "T04 T24", measured: M2026 },- OpFact { id: "sketch.offset.top.negative", what: "a sketch on a plane offset − from Top Plane (keeps Top Plane's axes)", checks: "T05 T26", measured: M2026 },+ OpFact { id: "sketch.offset.top.positive", what: "a sketch on a plane offset + from Top Plane", checks: "T04 T24", measured: M2026_2021 },+ OpFact { id: "sketch.offset.top.negative", what: "a sketch on a plane offset − from Top Plane (keeps Top Plane's axes)", checks: "T05 T26", measured: M2026_2021 }, OpFact { id: "sketch.offset.front.positive", what: "a sketch on a plane offset + from Front Plane", checks: "T38", measured: M2026B }, OpFact { id: "sketch.offset.front.negative", what: "a sketch on a plane offset − from Front Plane", checks: "not in the live battery", measured: UNMEASURED }, OpFact { id: "sketch.offset.right.positive", what: "a sketch on a plane offset + from Right Plane", checks: "not in the live battery", measured: UNMEASURED }, OpFact { id: "sketch.offset.right.negative", what: "a sketch on a plane offset − from Right Plane", checks: "not in the live battery", measured: UNMEASURED }, OpFact { id: "sketch.offset.named", what: "a sketch on a plane offset from a named plane", checks: "not in the live battery", measured: UNMEASURED },- OpFact { id: "sketch.face.top.positive", what: "a sketch on a face facing +Y (a top face), picked by a point", checks: "T06 T20 T21 T28 T32 T39", measured: M2026 },- OpFact { id: "sketch.face.top.negative", what: "a sketch on a face facing −Y (a bottom face, hidden in an iso view)", checks: "T25", measured: M2026 },+ OpFact { id: "sketch.face.top.positive", what: "a sketch on a face facing +Y (a top face), picked by a point", checks: "T06 T20 T21 T28 T32 T39", measured: M2026_2021 },+ OpFact { id: "sketch.face.top.negative", what: "a sketch on a face facing −Y (a bottom face, hidden in an iso view)", checks: "T25", measured: M2026_2021 }, OpFact { id: "sketch.face.right.positive", what: "a sketch on a face facing +X (keeps Right Plane's axes, the origin projected)", checks: "T36", measured: M2026B }, OpFact { id: "sketch.face.right.negative", what: "a sketch on a face facing −X", checks: "not in the live battery", measured: UNMEASURED }, OpFact { id: "sketch.face.front.negative", what: "a sketch on a face facing −Z (keeps Front Plane's axes, the origin projected)", checks: "T37", measured: M2026B }, OpFact { id: "sketch.face.front.positive", what: "a sketch on a face facing +Z", checks: "not in the live battery", measured: UNMEASURED },- OpFact { id: "sketch.rect", what: "a rectangle (corner or center anchor)", checks: "T01 T02 T03", measured: M2026 },- OpFact { id: "sketch.circle", what: "a circle", checks: "T04 T06 T30", measured: M2026 },- OpFact { id: "sketch.multiProfile", what: "several profiles in one sketch (a ring, disjoint rectangles, the LEGO tubes)", checks: "T13 T14 T24", measured: M2026 },- OpFact { id: "extrude.one", what: "a boss one way (the plane's normal, or outward from a face)", checks: "T01 T06 T25 and every base block", measured: M2026 },+ OpFact { id: "sketch.rect", what: "a rectangle (corner or center anchor)", checks: "T01 T02 T03", measured: M2026_2021 },+ OpFact { id: "sketch.circle", what: "a circle", checks: "T04 T06 T30", measured: M2026_2021 },+ OpFact { id: "sketch.multiProfile", what: "several profiles in one sketch (a ring, disjoint rectangles, the LEGO tubes)", checks: "T13 T14 T24", measured: M2026_2021 },+ OpFact { id: "extrude.one", what: "a boss one way (the plane's normal, or outward from a face)", checks: "T01 T06 T25 and every base block", measured: M2026_2021 }, OpFact { id: "extrude.one.reversed", what: "a boss whose direction flag is reversed (reverse:true: from Top Plane it goes −Y)", checks: "T33", measured: M2026B },- OpFact { id: "extrude.both", what: "a boss both ways (depthMm and depth2Mm)", checks: "T10", measured: M2026 },- OpFact { id: "extrude.midPlane", what: "a boss symmetric about the sketch plane", checks: "T11", measured: M2026 },+ OpFact { id: "extrude.both", what: "a boss both ways (depthMm and depth2Mm)", checks: "T10", measured: M2026_2021 },+ OpFact { id: "extrude.midPlane", what: "a boss symmetric about the sketch plane", checks: "T11", measured: M2026_2021 }, OpFact { id: "extrude.one.throughAll", what: "a boss through all, one way", checks: "T34", measured: M2026B }, OpFact { id: "extrude.both.throughAll", what: "a boss through all, both ways", checks: "not in the live battery", measured: UNMEASURED },- OpFact { id: "cut.one", what: "a cut one way, the default direction (the plane's −normal, into a face)", checks: "T07 (nothing on that side: the step failed, as it must) and T27's first direction", measured: M2026 },- OpFact { id: "cut.one.reversed", what: "a cut one way, reversed (reverse:true)", checks: "T08 T24", measured: M2026 },- OpFact { id: "cut.one.throughAll.reversed", what: "a cut through all, reversed", checks: "T09", measured: M2026 },- OpFact { id: "cut.both", what: "a cut both ways (depthMm along the cut's own direction, depth2Mm the other)", checks: "T27", measured: M2026 },- OpFact { id: "cut.midPlane", what: "a cut symmetric about the sketch plane", checks: "T12", measured: M2026 },- OpFact { id: "select.multiSelectTyped", what: "the picked entities selected by MultiSelect2 with a typed object array", checks: "11 of 11 live selections (T06 T16-T21 T25 T28-T30)", measured: M2026 },+ OpFact { id: "cut.one", what: "a cut one way, the default direction (the plane's −normal, into a face)", checks: "T07 (nothing on that side: the step failed, as it must) and T27's first direction", measured: M2026_2021 },+ OpFact { id: "cut.one.reversed", what: "a cut one way, reversed (reverse:true)", checks: "T08 T24", measured: M2026_2021 },+ OpFact { id: "cut.one.throughAll.reversed", what: "a cut through all, reversed", checks: "T09", measured: M2026_2021 },+ OpFact { id: "cut.both", what: "a cut both ways (depthMm along the cut's own direction, depth2Mm the other)", checks: "T27", measured: M2026_2021 },+ OpFact { id: "cut.midPlane", what: "a cut symmetric about the sketch plane", checks: "T12", measured: M2026_2021 },+ OpFact { id: "select.multiSelectTyped", what: "the picked entities selected by MultiSelect2 with a typed object array", checks: "11 of 11 live selections (T06 T16-T21 T25 T28-T30)", measured: M2026_2021 }, OpFact { id: "select.entitySelect4", what: "a picked entity selected by IEntity::Select4 (the ladder's 2nd rung)", checks: "never needed live: the first rung worked", measured: UNMEASURED }, OpFact { id: "select.selectByRay", what: "a picked entity selected by SelectByRay (the ladder's 3rd rung)", checks: "never needed live: the first rung worked", measured: UNMEASURED }, OpFact { id: "select.selectByIdAtPoint", what: "a picked entity selected by SelectByID2 at the point, through the view (the last rung)", checks: "never needed live: the first rung worked", measured: UNMEASURED },- OpFact { id: "fillet.allOfFeature", what: "a fillet on every edge of a feature (a box's 12, a cylinder's 2)", checks: "T17 T30", measured: M2026 },- OpFact { id: "fillet.atPoints", what: "a fillet on edges picked by points", checks: "T32 (swx_selftest)", measured: M2026 },- OpFact { id: "fillet.FeatureFillet3", what: "the fillet made by IFeatureManager::FeatureFillet3", checks: "T17 T30 T32", measured: M2026 },+ OpFact { id: "fillet.allOfFeature", what: "a fillet on every edge of a feature (a box's 12, a cylinder's 2)", checks: "T17 T30", measured: M2026_2021 },+ OpFact { id: "fillet.atPoints", what: "a fillet on edges picked by points", checks: "T32 (swx_selftest)", measured: M2026_2021 },+ OpFact { id: "fillet.FeatureFillet3", what: "the fillet made by IFeatureManager::FeatureFillet3", checks: "T17 T30 T32", measured: M2026_2021 }, OpFact { id: "fillet.createDefinition", what: "the fillet made by the CreateDefinition(swFmFillet) fallback", checks: "never needed live: FeatureFillet3 worked", measured: UNMEASURED },- OpFact { id: "chamfer.atPoints", what: "a chamfer on edges picked by points", checks: "T18", measured: M2026 },+ OpFact { id: "chamfer.atPoints", what: "a chamfer on edges picked by points", checks: "T18", measured: M2026_2021 }, OpFact { id: "chamfer.allOfFeature", what: "a chamfer on every edge of a feature", checks: "not in the live battery", measured: UNMEASURED },- OpFact { id: "shell.inward", what: "a shell inward, removing faces picked by points (the top face, and the bottom face hidden in an iso view)", checks: "T19 T29", measured: M2026 },+ OpFact { id: "shell.inward", what: "a shell inward, removing faces picked by points (the top face, and the bottom face hidden in an iso view)", checks: "T19 T29", measured: M2026_2021 }, OpFact { id: "shell.outward", what: "a shell outward (the original block becomes the cavity)", checks: "T42", measured: M2026B },- OpFact { id: "linear_pattern.x", what: "a linear pattern along X, one direction (positive or negative spacing)", checks: "T20 T28", measured: M2026 },+ OpFact { id: "linear_pattern.x", what: "a linear pattern along X, one direction (positive or negative spacing)", checks: "T20 T28", measured: M2026_2021 }, OpFact { id: "linear_pattern.x+z", what: "a linear pattern in two directions, X and Z (positive and negative spacing)", checks: "T39", measured: M2026B },- OpFact { id: "circular_pattern.y", what: "a circular pattern about Y", checks: "T21", measured: M2026 },- OpFact { id: "set_appearance.part", what: "a colour on the part (or on the one body of a single-body part)", checks: "T15 T24", measured: M2026 },- OpFact { id: "set_appearance.face", what: "a colour on one face picked by a point", checks: "T16", measured: M2026 },+ OpFact { id: "circular_pattern.y", what: "a circular pattern about Y", checks: "T21", measured: M2026_2021 },+ OpFact { id: "set_appearance.part", what: "a colour on the part (or on the one body of a single-body part)", checks: "T15 T24", measured: M2026_2021 },+ OpFact { id: "set_appearance.face", what: "a colour on one face picked by a point", checks: "T16", measured: M2026_2021 }, OpFact { id: "set_appearance.body", what: "a colour on one body of a multi-body part", checks: "not in the live battery", measured: UNMEASURED },- OpFact { id: "set_material", what: "a material from the SOLIDWORKS library (the body's density follows it)", checks: "T22 T24", measured: M2026 },- OpFact { id: "set_material.reappliesAppearance", what: "a material after a colour: the bridge re-applies the colour", checks: "T31 (swx_selftest)", measured: M2026 },- OpFact { id: "set_prop", what: "a custom property", checks: "T24", measured: M2026 },+ OpFact { id: "set_material", what: "a material from the SOLIDWORKS library (the body's density follows it)", checks: "T22 T24", measured: M2026_2021 },+ OpFact { id: "set_material.reappliesAppearance", what: "a material after a colour: the bridge re-applies the colour", checks: "T31 (swx_selftest)", measured: M2026_2021 },+ OpFact { id: "set_prop", what: "a custom property", checks: "T24", measured: M2026_2021 }, OpFact { id: "save_as", what: "save_as as a script step", checks: "not in the live battery (swx_save_as, the verb, is)", measured: UNMEASURED }, OpFact { id: "rebuild", what: "rebuild as a script step", checks: "not in the live battery", measured: UNMEASURED }, OpFact { id: "zoom_fit", what: "zoom_fit as a script step", checks: "not in the live battery", measured: UNMEASURED },@@ -474,8 +486,9 @@ mod tests { assert!(missing_on(24).contains(&"IFeatureManager::FeatureCut4")); let d = describe(Some("29.5.1")); assert_eq!(d["supported"], true);- assert_eq!(d["tested"], false);+ assert_eq!(d["tested"], true, "2021: a community run (0.10.10)"); assert_eq!(d["year"], 2021);+ assert_eq!(describe(Some("30.1.0"))["tested"], false, "2022 has never been run"); } fn ops(script: serde_json::Value) -> Vec<crate::script::Op> {@@ -495,7 +508,7 @@ mod tests { // The live run of 2026-10-14, exactly as the coordinator recorded it. assert_eq!( LIVE_2026_10_14.to_json(),- serde_json::json!({"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.3"})+ serde_json::json!({"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.3", "by": "adom"}) ); // What docs/LIVE-RESULTS.md lists as measured... for id in [@@ -506,8 +519,22 @@ mod tests { "fillet.allOfFeature", "fillet.FeatureFillet3", "chamfer.atPoints", "shell.inward", "linear_pattern.x", "circular_pattern.y", "set_prop", "select.multiSelectTyped", ] {- assert_eq!(op_fact(id).map(|f| f.measured), Some(M2026), "{id} was measured on 2026");+ assert_eq!(op_fact(id).map(|f| f.measured), Some(M2026_2021), "{id} was measured on 2026, and on 2021 by the community run");+ }+ // 2021 (0.10.10): Michael's community run, and EXACTLY the facts its+ // selftest checks (T01-T22, T24-T32) measured — plus new_part and the+ // shared part's clear_part; nothing from T33+ (not in the self-test).+ assert_eq!(+ LIVE_2021_10_15.to_json(),+ serde_json::json!({"solidworks": 2021, "major": 29, "edition": "desktop", "date": "2026-10-15", "bridge": "0.10.9", "by": "community"})+ );+ let mut on_2021: Vec<&str> = OP_FACTS.iter().filter(|f| f.measured.iter().any(|m| m.major == 29)).map(|f| f.id).collect();+ on_2021.sort_unstable();+ assert_eq!(on_2021.len(), 32, "{on_2021:?}");+ for id in ["sketch.plane.right", "extrude.one.reversed", "shell.outward", "linear_pattern.x+z"] {+ assert!(!on_2021.contains(&id), "{id} is a T33+ check, never run on 2021"); }+ assert!(TESTED_MAJOR.contains(&29) && TESTED_MAJOR.contains(&34)); // ...and the eight checks added with 0.10.4 (T33-T39, T42), measured the same day. for id in [ "extrude.one.reversed", "extrude.one.throughAll", "sketch.plane.right", "sketch.face.right.positive",@@ -517,7 +544,7 @@ mod tests { } assert_eq!( LIVE_2026_10_14_B.to_json(),- serde_json::json!({"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.4"})+ serde_json::json!({"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.4", "by": "adom"}) ); // ...and EXACTLY what is not: its "not yet measured" list (throughAll+both // on a boss, the ladder's later rungs, the CreateDefinition fillet) plus@@ -609,12 +636,15 @@ mod tests { // The coordinator's shape, on SOLIDWORKS 2026. let p = proof(&v(&["sketch.face.top.positive", "sketch.circle", "select.multiSelectTyped"]), Some(34)); assert_eq!(p["live"], true);- assert_eq!(p["measuredOn"], serde_json::json!([LIVE_2026_10_14.to_json()]));+ assert_eq!(p["measuredOn"], serde_json::json!([LIVE_2026_10_14.to_json(), LIVE_2021_10_15.to_json()])); assert!(p.get("why").is_none() && p.get("unmeasured").is_none());- // The same step on SOLIDWORKS 2021: measured elsewhere, not live here.- let p = proof(&v(&["sketch.face.top.positive", "sketch.circle"]), Some(29));+ // ...and on SOLIDWORKS 2021 too, by the community run (0.10.10).+ assert_eq!(proof(&v(&["sketch.face.top.positive", "sketch.circle"]), Some(29))["live"], true);+ // A step measured only by 0.10.4's T33+ checks, on 2021: measured+ // elsewhere, not live here.+ let p = proof(&v(&["sketch.face.right.positive", "sketch.circle"]), Some(29)); assert_eq!(p["live"], false);- assert_eq!(p["measuredOn"].as_array().map(|a| a.len()), Some(1));+ assert_eq!(p["measuredOn"].as_array().map(|a| a.len()), Some(2)); assert!(p["why"].as_str().unwrap_or("").contains("this is SOLIDWORKS 2021"), "{p}"); // A version that could not be read is never live. assert_eq!(proof(&v(&["new_part"]), None)["live"], false);@@ -635,7 +665,7 @@ mod tests { // The table, as describe and selftest report it. let t = live_table(Some(29)); assert_eq!(t["variants"].as_array().map(|a| a.len()), Some(OP_FACTS.len()));- assert!(t["variants"].as_array().unwrap().iter().all(|r| r["liveOnThisInstall"] == false), "nothing is live on 2021 yet");+ assert_eq!(t["variants"].as_array().unwrap().iter().filter(|r| r["liveOnThisInstall"] == true).count(), 32, "on 2021: the 32 variants the community run measured"); let t = live_table(Some(34)); let live = t["variants"].as_array().unwrap().iter().filter(|r| r["liveOnThisInstall"] == true).count(); assert_eq!(live, t["measuredVariants"].as_u64().unwrap() as usize);
src/selftest.rs+162−9@@ -28,6 +28,95 @@ 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 {@@ -72,6 +161,14 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { } 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!({}));@@ -130,6 +227,12 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { } 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);@@ -245,6 +348,7 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { } // 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 {@@ -254,9 +358,22 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { 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 = json!({"targetDocument": title});+ let mut a = addr.clone(); if let (Value::Object(m), Value::Object(e)) = (&mut a, extra) { m.extend(e); }@@ -272,7 +389,7 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"}, {"op": "extrude", "depthMm": 10} ]);- let block = run.call(ctx, "issue #2: a script into the bridge's own part (by path) says bridgeCreatedDocument:true", "swx_run_script", sargs);+ 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);@@ -291,7 +408,24 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { 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!({})));- run.call(ctx, "close the scratch part", "swx_close_doc", target(json!({"save": false})));+ 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)"); }@@ -302,7 +436,11 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { // 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()));+ 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)@@ -453,18 +591,24 @@ fn make_shared_document(ctx: &Ctx, over: Option<&str>) -> Option<String> { a["reason"] = json!(r); } let made = crate::verbs::dispatch(ctx, "swx_new_document", &a);- made.pointer("/documentIdentity/path")+ 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)+ .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 key = if t.contains('\\') || t.contains('/') { "path" } else { "targetDocument" };- let _ = crate::verbs::dispatch(ctx, "swx_close_doc", &json!({key: t, "save": false}));+ 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);+ } } } @@ -638,6 +782,7 @@ fn finish_base(level: &str, sw: &Value, ready: bool, running: bool, started: Ins "summary": {"passed": passed, "failed": failed, "skipped": skipped, "solidworksRunning": running, "seconds": started.elapsed().as_secs_f64()}, "results": run.results, "_hint": hint,+ "_timeoutHint": TIMEOUT_HINT, })) } @@ -930,6 +1075,11 @@ fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<& } 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;@@ -1069,7 +1219,10 @@ fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<& } // 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 _ = crate::verbs::dispatch(ctx, "swx_close_doc", &json!({"targetDocument": title, "save": false}));+ 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,
src/verbs.rs+54−6@@ -97,6 +97,30 @@ pub fn same_path(a: &str, b: &str) -> bool { !a.is_empty() && !b.is_empty() && norm(a) == norm(b) } +/// A document TITLE without its SOLIDWORKS extension (0.10.10). SOLIDWORKS'+/// `GetTitle` follows Windows Explorer's "Hide extensions for known file+/// types" (HideFileExt): with it ON — the Windows DEFAULT — a saved part is+/// titled `Part14-17`, with it off `Part14-17.SLDPRT`. MEASURED 2026-10-15 on+/// Michael's SOLIDWORKS 2021: every verb the self-test aimed at its scratch+/// part by the title WITH the extension failed `document_not_open`. Only a+/// trailing `.sldprt` / `.sldasm` / `.slddrw` (any case) is removed: a dot+/// elsewhere (`Rev1.2`) is part of the name.+pub fn title_stem(t: &str) -> &str {+ let t = t.trim_end();+ for ext in [".sldprt", ".sldasm", ".slddrw"] {+ if t.len() > ext.len() && t.is_char_boundary(t.len() - ext.len()) && t[t.len() - ext.len()..].eq_ignore_ascii_case(ext) {+ return &t[..t.len() - ext.len()];+ }+ }+ t+}++/// THE ONE TITLE COMPARISON (0.10.10): equal once the SOLIDWORKS extension is+/// dropped from both sides — a title match must never depend on HideFileExt.+pub fn same_title(a: &str, b: &str) -> bool {+ !a.trim().is_empty() && !b.trim().is_empty() && title_stem(a) == title_stem(b)+}+ /// IS THIS OPEN DOCUMENT THE ONE WE RECORDED? /// /// The single comparison both the ownership check and the prune must use.@@ -120,7 +144,7 @@ pub fn doc_matches( ) -> bool { match (stored_path, open_path) { (Some(a), Some(b)) => same_path(a, b),- (None, None) => !stored_title.is_empty() && stored_title == open_title,+ (None, None) => same_title(stored_title, open_title), _ => false, } }@@ -204,7 +228,7 @@ impl Ctx { } { let mut v = self.scratch_docs.lock().unwrap_or_else(|e| e.into_inner());- if !v.iter().any(|d| d.title == title && d.session == session) {+ if !v.iter().any(|d| same_title(&d.title, title) && d.session == session) { v.push(ScratchDoc { title: title.to_string(), session, path: None }); } }@@ -245,7 +269,7 @@ impl Ctx { #[cfg(test)] pub(crate) fn forget_scratch(&self, title: &str) {- self.scratch_docs.lock().unwrap_or_else(|e| e.into_inner()).retain(|d| d.title != title);+ self.scratch_docs.lock().unwrap_or_else(|e| e.into_inner()).retain(|d| !same_title(&d.title, title)); self.persist_scratch(); } @@ -278,7 +302,7 @@ impl Ctx { let mut v = self.scratch_docs.lock().unwrap_or_else(|e| e.into_inner()); let from_path = from_path.filter(|p| !p.is_empty()); let hit = v.iter().position(|d| doc_matches(&d.title, d.path.as_deref(), from, from_path)).or_else(|| {- if title_fallback { v.iter().position(|d| d.title == from) } else { None }+ if title_fallback { v.iter().position(|d| same_title(&d.title, from)) } else { None } }); if let Some(d) = hit.map(|i| &mut v[i]) { d.title = to.to_string();@@ -647,6 +671,9 @@ fn swx_status(ctx: &Ctx, _args: &Value) -> Value { // IS THE USER AT THE MACHINE? (0.10.9) Read without COM, every time, so a // battery can check it in the same call as memory. "userPresence": crate::presence::activity().to_json(),+ // A self-test cut off by a client timeout leaves its parts open+ // (measured 2026-10-15): REPORTED here, closed by the next swx_selftest.+ "orphanedSelftestDocuments": crate::selftest::orphan_report(), "windowEtiquette": "background", "_hint": hint, }))@@ -2186,7 +2213,7 @@ fn doc_is_dirty(ctx: &Ctx, target: &crate::verbs::Target) -> Option<bool> { let docs = ctx.call("docs", json!({}), 30).ok()?; let list = docs.get("docs")?.as_array()?; let found = list.iter().find(|x| {- x.get("title").and_then(|t| t.as_str()) == Some(target.title.as_str())+ x.get("title").and_then(|t| t.as_str()).map(|t| same_title(t, &target.title)).unwrap_or(false) && x.get("path").and_then(|p| p.as_str()).filter(|s| !s.is_empty()).map(String::from) == target.path })?;@@ -3149,7 +3176,7 @@ pub const CATALOG: &[(&str, &str, &str)] = &[ ("swx_inspect_bodies", "read", "per-body geometry read-back in millimetres (bbox, volume, area, faces)"), ("swx_run_macro", "exec", "run a SOLIDWORKS VBA macro (.swp/.swb/.dll) — arbitrary code, so it needs the write opt-in every time"), ("swx_run_script", "exec", "the modeling DSL in millimetres: new_part, sketch (several rect/circle profiles in one sketch, on a named plane, an offset plane {offsetFrom, offsetMm} or a face {atPointMm}), sketch_rect/sketch_circle (anchor corner|center), extrude/cut (reverse, direction one|both|midPlane, throughAll), fillet, chamfer, shell, linear_pattern, circular_pattern, set_appearance (rgb), set_material, set_prop, save_as; a feature SOLIDWORKS returns null is an error (refuses to build into a document the bridge did not create)"),- ("swx_selftest", "exec", "one JSON that says, verb by verb, what works on THIS SOLIDWORKS install: level:\"read\" (default) changes nothing; level:\"write\" also builds, reads back, exports, saves and CLOSES a scratch part in the bridge's own scratch folder, one modelling op per call — how a user on a release we have not run (e.g. SOLIDWORKS 2021) gets first-class support"),+ ("swx_selftest", "exec", "LONG-RUNNING: call it with responseMode:\"stream\" or \"job\" — level write runs 5-8 minutes, and a client's default 120 s budget cuts it off (a cut-off run's parts are closed by the next swx_selftest, and swx_status lists them). One JSON that says, verb by verb, what works on THIS SOLIDWORKS install: level:\"read\" (default) changes nothing; level:\"write\" also builds, reads back, exports, saves and CLOSES a scratch part in the bridge's own scratch folder, one modelling op per call — how a user on a release we have not run (e.g. SOLIDWORKS 2021) gets first-class support"), ("swx_add_component", "write", "insert part/assembly files as components of an assembly, positioned in mm (the verb swx_bom needs)"), ("swx_get_preferences", "read", "the small set of preferences this bridge can round-trip"), ("swx_set_preference", "write", "write one preference and READ IT BACK to verify"),@@ -3715,6 +3742,27 @@ fn dispatch_after_gate(ctx: &Ctx, command: &str, args: &Value) -> Value { #[cfg(test)] mod tests {+ #[test]+ fn titles_compare_without_the_solidworks_extension() {+ use super::{same_title, title_stem};+ // MEASURED 2026-10-15 (Michael, SOLIDWORKS 2021, HideFileExt=1).+ assert!(same_title("Part14-1792088642002.SLDPRT", "Part14-1792088642002"));+ assert!(same_title("Part14-1792088642002", "Part14-1792088642002.SLDPRT"));+ assert!(same_title("gearbox.sldasm", "gearbox.SLDASM"), "the extension's case does not matter");+ assert!(same_title("sheet.SldDrw", "sheet"));+ assert!(!same_title("Part1", "part1"), "the NAME is compared exactly");+ assert!(!same_title("Part1", "Part12"));+ assert!(!same_title("", ""), "an empty title names nothing");+ // A dot elsewhere is part of the name, never an extension.+ assert_eq!(title_stem("Bracket Rev1.2"), "Bracket Rev1.2");+ assert_eq!(title_stem("Bracket Rev1.2.SLDPRT"), "Bracket Rev1.2");+ assert!(!same_title("Bracket Rev1.2", "Bracket Rev1"));+ assert_eq!(title_stem("drawing.pdf"), "drawing.pdf", "only SOLIDWORKS' own extensions");+ assert_eq!(title_stem(".SLDPRT"), ".SLDPRT", "an extension alone is not stripped to nothing");+ // Ownership of an unsaved document follows the same rule.+ assert!(super::doc_matches("Part3.SLDPRT", None, "Part3", None));+ }+ use super::*; #[test]
src/verbs3.rs+13−5@@ -189,7 +189,7 @@ fn resolve_target_inner( let ambiguous = |title: &str| -> Value { let places: Vec<Value> = open .iter()- .filter(|d| doc_title(d) == title)+ .filter(|d| crate::verbs::same_title(&doc_title(d), title)) .map(|d| json!({"title": doc_title(d), "path": doc_path(d)})) .collect(); err_code(@@ -207,7 +207,7 @@ fn resolve_target_inner( }; let by_title = |t: &str| -> Result<Option<Value>, Value> {- let hits: Vec<&Value> = open.iter().filter(|d| doc_title(d) == t).collect();+ let hits: Vec<&Value> = open.iter().filter(|d| crate::verbs::same_title(&doc_title(d), t)).collect(); match hits.len() { 0 => Ok(None), 1 => Ok(Some(hits[0].clone())),@@ -2347,7 +2347,7 @@ pub fn close_all_documents(ctx: &Ctx, args: &Value) -> Value { } for d in r.get("remaining").and_then(|v| v.as_array()).cloned().unwrap_or_default() { let title = d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();- if named.iter().any(|n| *n == title) {+ if named.iter().any(|n| crate::verbs::same_title(n, &title)) { continue; } let dirty = d.get("dirty").and_then(|x| x.as_bool());@@ -3759,7 +3759,7 @@ pub fn verb_times(_ctx: &Ctx, _args: &Value) -> Value { /// pathless document by title — the registry drain and the workspace sweep — and /// a rule enforced in one of them is a rule with a hole in it. fn title_resolves_uniquely(open_docs: &[Value], title: &str) -> bool {- open_docs.iter().filter(|o| doc_title(o).eq_ignore_ascii_case(title)).count() == 1+ open_docs.iter().filter(|o| crate::verbs::title_stem(&doc_title(o)).eq_ignore_ascii_case(crate::verbs::title_stem(title))).count() == 1 } fn give_identity(ctx: &Ctx, title: &str, reported_path: Option<&str>, kind: &str) -> Value {@@ -3795,7 +3795,15 @@ fn give_identity(ctx: &Ctx, title: &str, reported_path: Option<&str>, kind: &str // THE TITLE CHANGES WITH THE FILE. A real Save As renames the open // document to the file it now is, so the caller — and the ownership // registry — must be told the name it will actually answer to.- let new_title = crate::comhost::mock::base_name(&saved);+ // READ BACK, never synthesized from the file name (0.10.10): with+ // Windows hiding known extensions (HideFileExt=1, the default)+ // SOLIDWORKS titles it `Part14-17`, otherwise `Part14-17.SLDPRT`.+ let new_title = r+ .pointer("/doc/title")+ .and_then(|t| t.as_str())+ .filter(|t| !t.trim().is_empty())+ .map(String::from)+ .unwrap_or_else(|| crate::comhost::mock::base_name(&saved).to_string()); json!({ "identity": "path", "path": saved,
tests/hidefileext.rsadded+124@@ -0,0 +1,124 @@+//! HIDEFILEEXT (0.10.10). MEASURED 2026-10-15 on Michael's SOLIDWORKS 2021+//! (COM 29.5.1, Windows 11): swx_selftest {level:"write"} passed every+//! modelling check and failed SIX verbs — swx_design_info, swx_get_parameters,+//! swx_set_parameter, swx_export_step, swx_save, swx_close_doc — all+//! `document_not_open: no open document is titled 'Part14-….SLDPRT'`. With+//! Windows' "Hide extensions for known file types" ON (the Windows default),+//! SOLIDWORKS titles that document `Part14-…`; the self-test addressed it by a+//! title WITH the extension, and the close missed too, leaving it open.+//!+//! `mock-hide-file-ext` makes the simulator title documents the way that+//! machine did. Everything here runs in the simulator.++mod common;++use common::{Bridge, TempDir};+use serde_json::{json, Value};++fn s(v: &Value) -> String {+ serde_json::to_string_pretty(v).unwrap_or_default()+}++fn boot(tag: &str, hide: bool) -> (Bridge, TempDir) {+ let machine = TempDir::new(tag);+ std::fs::write(machine.join("windows.json"), "[]").unwrap();+ let b = Bridge::start_with(&[("SWX_LAUNCH_FAKE_DIR", &machine.path())]);+ if hide {+ std::fs::write(b.scratch.join("mock-hide-file-ext"), "1").unwrap();+ }+ std::fs::write(b.scratch.join("mock-revision"), "29.5.1").unwrap();+ b.call("swx_start", json!({}));+ (b, machine)+}++#[test]+fn the_selftest_passes_with_extensions_hidden_and_shown_and_leaves_nothing_open() {+ for hide in [true, false] {+ let (b, _m) = boot(if hide { "hfe-selftest-hidden" } else { "hfe-selftest-shown" }, hide);+ let before = b.call("swx_docs", json!({}))["count"].clone();+ let r = b.call("swx_selftest", json!({"level": "write"}));+ let rows = r["results"].as_array().cloned().unwrap_or_default();+ let failed: Vec<&Value> = rows.iter().filter(|x| x["pass"] == false).collect();+ check!(+ b,+ &format!("swx_selftest write, HideFileExt {}: nothing fails (Michael's 6 failures were one bug)", if hide { "ON" } else { "off" }),+ r["success"] == true && failed.is_empty(),+ format!("{}\nFAILED: {failed:?}", s(&r["summary"]))+ );+ for verb in ["swx_design_info", "swx_get_parameters", "swx_set_parameter", "swx_export_step", "swx_save", "swx_close_doc"] {+ check!(+ b,+ &format!("{verb} on the scratch part passes (HideFileExt {hide})"),+ rows.iter().any(|x| x["verb"] == verb && x["pass"] == true),+ s(&Value::Array(rows.clone()))+ );+ }+ check!(+ b,+ "the ownership check is numbered T23, so the rows run T01-T32 without a gap",+ rows.iter().any(|x| x["name"].as_str().unwrap_or("").starts_with("T23 ")),+ ""+ );+ check!(b, "every scratch part was CLOSED", b.call("swx_docs", json!({}))["count"] == before && r.get("documentsLeftOpen").is_none(), s(&r));+ check!(b, "...and struck off the orphan record", b.call("swx_status", json!({}))["orphanedSelftestDocuments"].is_null(), "");+ check!(+ b,+ "the reply says how to call it without being cut off",+ r["_timeoutHint"].as_str().unwrap_or("").starts_with("call this with responseMode:\"stream\" or \"job\""),+ s(&r["_timeoutHint"])+ );+ }+}++#[test]+fn a_title_with_or_without_the_extension_names_the_same_document() {+ let (b, _m) = boot("hfe-titles", true);+ let made = b.call("swx_new_document", json!({"type": "part"}));+ let title = made["created"]["title"].as_str().unwrap_or("").to_string();+ let path = made["documentIdentity"]["path"].as_str().unwrap_or("").to_string();+ check!(+ b,+ "swx_new_document reports the title SOLIDWORKS shows (no extension with HideFileExt ON) AND the path",+ !title.is_empty() && !title.to_ascii_lowercase().ends_with(".sldprt") && path.to_ascii_lowercase().ends_with(".sldprt"),+ s(&made)+ );+ let docs = b.call("swx_docs", json!({}));+ check!(b, "swx_docs lists it under that same title", docs.to_string().contains(&format!("\"{title}\"")), s(&docs));+ let with_ext = format!("{title}.SLDPRT");+ let di = b.call("swx_design_info", json!({"targetDocument": with_ext}));+ check!(b, "swx_design_info finds it by the title WITH the extension", di["success"] != false, s(&di));+ let lower = format!("{title}.sldprt");+ let gp = b.call("swx_get_parameters", json!({"targetDocument": lower}));+ check!(b, "...and with a lower-case extension", gp["success"] != false, s(&gp));+ let act = b.call("swx_activate_document", json!({"name": with_ext}));+ check!(b, "swx_activate_document takes either spelling", act["success"] != false, s(&act));+ let close = b.call("swx_close_doc", json!({"targetDocument": with_ext, "save": false}));+ check!(b, "swx_close_doc closes it by the title with the extension", close["success"] != false, s(&close));+ check!(b, "...and it is gone", b.call("swx_docs", json!({}))["count"] == 0, "");+}++#[test]+fn a_cut_off_selftests_parts_are_reported_by_status_and_closed_by_the_next_run() {+ let (b, _m) = boot("hfe-orphans", true);+ // What a cut-off run leaves: a part it made and recorded, still open.+ let made = b.call("swx_new_document", json!({"type": "part"}));+ let path = made["documentIdentity"]["path"].as_str().unwrap_or("").to_string();+ std::fs::write(b.scratch.join("selftest-docs.json"), json!([{"key": path, "at": "2026-10-15T00:00:00Z"}]).to_string()).unwrap();+ let st = b.call("swx_status", json!({}));+ check!(+ b,+ "swx_status REPORTS the orphan (and closes nothing: a run may be in progress)",+ st["orphanedSelftestDocuments"]["documents"][0]["key"] == path.as_str() && b.call("swx_docs", json!({}))["count"] == 1,+ s(&st["orphanedSelftestDocuments"])+ );+ let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01"]}));+ let rows = r["results"].as_array().cloned().unwrap_or_default();+ check!(+ b,+ "the next swx_selftest closes it first, and says so",+ rows.iter().any(|x| x["name"] == "close what a cut-off self-test left open" && x["pass"] == true),+ s(&Value::Array(rows.clone()))+ );+ check!(b, "nothing is left open, nothing left recorded", b.call("swx_docs", json!({}))["count"] == 0+ && b.call("swx_status", json!({}))["orphanedSelftestDocuments"].is_null(), "");+}
tests/issue1.rs+20−12@@ -89,7 +89,12 @@ fn the_lego_brick_is_four_features_with_the_new_ops() { && near(&body["massKg"], 2073.3615e-9 * 1020.0, 1e-8), s(body) );- check!(b, "the example says it is UNMEASURED", brick["expected"]["status"].as_str().unwrap_or("").starts_with("UNMEASURED"), s(&brick["expected"]));+ check!(+ b,+ "the example says where it was MEASURED: SOLIDWORKS 2026 and 2021 (community), identical",+ brick["expected"]["status"].as_str().map(|t| t.starts_with("MEASURED on SOLIDWORKS 2026") && t.contains("SOLIDWORKS 2021") && t.contains("community")).unwrap_or(false),+ s(&brick["expected"])+ ); } #[test]@@ -197,9 +202,9 @@ fn selftest_reports_every_check_and_cleans_up_after_itself() { let r = b.call("swx_selftest", json!({})); check!( b,- "read level: version facts (29 = SOLIDWORKS 2021: supported, not tested), rows, nothing failed",+ "read level: version facts (29 = SOLIDWORKS 2021: supported, and tested by a community run since 0.10.10), rows, nothing failed", r["success"] == true && r["level"] == "read" && r["solidworks"]["versionSupport"]["year"] == 2021- && r["solidworks"]["versionSupport"]["supported"] == true && r["solidworks"]["versionSupport"]["tested"] == false+ && r["solidworks"]["versionSupport"]["supported"] == true && r["solidworks"]["versionSupport"]["tested"] == true && r["summary"]["failed"] == 0 && r["results"].as_array().map(|a| a.len() >= 4).unwrap_or(false), s(&r) );@@ -384,7 +389,8 @@ fn steps_say_what_was_measured_live_on_this_release() { let r = b.call("swx_run_script", json!({"script": script})); check!(b, "the script runs", r["success"] == true, s(&r)); let st = r["steps"].as_array().cloned().unwrap_or_default();- let measured = json!([{"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.3"}]);+ let measured = json!([{"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.3", "by": "adom"},+ {"solidworks": 2021, "major": 29, "edition": "desktop", "date": "2026-10-15", "bridge": "0.10.9", "by": "community"}]); check!( b, "measured variants are LIVE on 2026, with the measurement: new part, Top Plane rect, boss, face sketch, shell",@@ -409,20 +415,22 @@ fn steps_say_what_was_measured_live_on_this_release() { "a REVERSED boss was measured by 0.10.4's checks (T33): live, with THAT measurement", st[9]["result"]["proven"]["live"] == true && st[9]["result"]["proven"]["measuredOn"]- == json!([{"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.4"}]),+ == json!([{"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.4", "by": "adom"}]), s(&st[9]) );- // The same script on SOLIDWORKS 2021: nothing is live there yet.+ // The same script on SOLIDWORKS 2021 (0.10.10): what Michael's community+ // self-test measured is live there too; what only 0.10.4's T33+ checks+ // measured (a reversed boss) is not, and says why. std::fs::write(b.scratch.join("mock-revision"), "29.5.1").unwrap(); let r = b.call("swx_run_script", json!({"script": script})); let st = r["steps"].as_array().cloned().unwrap_or_default(); check!( b,- "on 2021 every step is live:false, measured on 2026 only — and says so",+ "on 2021 the community-measured steps are live (both measurements listed); the reversed boss is live:false, measured on 2026 only — and says so", r["success"] == true- && st.iter().all(|x| x["result"]["proven"]["live"] == false)- && st[2]["result"]["proven"]["measuredOn"] == measured- && st[2]["result"]["proven"]["why"].as_str().unwrap_or("").contains("this is SOLIDWORKS 2021"),+ && [0usize, 1, 2, 3, 4, 5].iter().all(|i| st[*i]["result"]["proven"]["live"] == true && st[*i]["result"]["proven"]["measuredOn"] == measured)+ && st[9]["result"]["proven"]["live"] == false+ && st[9]["result"]["proven"]["why"].as_str().unwrap_or("").contains("this is SOLIDWORKS 2021"), s(&r) ); // One table: describe and selftest report what the steps report.@@ -431,10 +439,10 @@ fn steps_say_what_was_measured_live_on_this_release() { let rows = |v: &Value| v["liveMeasured"]["variants"].as_array().map(|a| a.len()).unwrap_or(0); check!( b,- "swx_describe and swx_selftest carry the same table; the selftest's rows say what is live on THIS install (2021: nothing)",+ "swx_describe and swx_selftest carry the same table; the selftest's rows say what is live on THIS install (2021: the 32 variants the community run measured)", rows(&d) > 20 && rows(&d) == rows(&t) && d["liveMeasured"]["variants"][0]["variant"] == t["liveMeasured"]["variants"][0]["variant"]- && t["liveMeasured"]["variants"].as_array().map(|a| a.iter().all(|r| r["liveOnThisInstall"] == false)).unwrap_or(false),+ && t["liveMeasured"]["variants"].as_array().map(|a| a.iter().filter(|r| r["liveOnThisInstall"] == true).count() == 32).unwrap_or(false), format!("{}\n{}", s(&d["liveMeasured"]), s(&t["liveMeasured"])) ); }
tools/cswa/SOURCES.mdadded+139@@ -0,0 +1,139 @@+# CSWA benchmark parts: sources, answer keys and rebuild recipes++Collected 2026-10-15. Every key is as published; every recipe was reproduced numerically (profile areas,+volumes, mass) by the reviewer before being written here, with the hypotheses that the drawings leave open+stated under "ambiguities". Tolerance: the official exam accepts an answer within 1 % of the key+("If you don't find an option within 1% of your answer please re-check your solid model"); keys are given to+two decimals. Units MMGS, "all holes through all unless shown otherwise".++## Sources++| Source | Status | URL |+|---|---|---|+| Dassault "CSWA Sample Exam" (2017 edition, 33 pp.; text identical to the 2013 edition and to Solid Solutions UK's Dec-2025 re-post, so still the current official sample) | OFFICIAL | https://www.solidworks.com/sites/default/files/2017-12/CSWASampleExam.pdf (archive: http://web.archive.org/web/20251230104817/https://www.solidworks.com/sites/default/files/2017-12/CSWASampleExam.pdf ; mirror https://www.solidsolutions.co.uk/wp-content/uploads/2025/12/SOLIDWORKS-Associate-CSWA-Sample-Exam.pdf) |+| Dassault CSWA sample 2013 (CAD Instructor Guide App. A), same questions and answers | OFFICIAL | https://www.solidworks.com/sw/docs/EDU_CSWA_Sample_Exam_2013_ENG.pdf |+| SolidWorks Corp 2007 sample, reseller-hosted | OFFICIAL content | https://cdn.goengineer.com/cswasampleexam2007.pdf |+| 3dengr "CSWA Practice Questions" (text-only geometry, worked answers) | COMMUNITY | https://www.3dengr.com/cswa-practice-questions.html |+| Planchard, Official CSWA Examination Guide, sample chapter (SDC) | COMMUNITY (book) | https://static.sdcpublications.com/pdfsample/978-1-58503-656-1-3.pdf |++Walkthroughs that confirm the Step 3/4 feature sequence: https://www.youtube.com/watch?v=eu1zbl1t7zc (13:25–23:47),+https://www.youtube.com/watch?v=oIsjTLH-GZg.++## Coordinate convention for the Tool Block (the exam says "Part origin: Arbitrary"; mass only)++Origin at the Ø14 hole centre, x right, y up, z = extrusion direction; "front" = the face the Step 3/4 cuts are made+from. Material AISI 1020 Steel, density 0.0079 g/mm³ (SOLIDWORKS library 7900 kg/m³).++## Part 1 — Tool Block, Step 1 (official, Q3; 2017 PDF p.18–19)++Key: **939.54 g** (option d); implied volume 118,929 mm³. Rebuilt: 939.54 g (0.00 %).+Variables: A = 81.00 (overall width), B = 57.00 (overall height), C = 43.00 (extrusion depth).++Profile sketch (Front plane), one closed loop + one circle:+- Ø14 through hole at (0,0): 14 from the left edge, 14 above the lobe-bottom line.+- Left lobe: left edge x = −14; bottom line y = −14; top y = +18 (lobe 32 tall); 45° chamfers 7×7 at both left+ corners, so the left edge runs y = −7 … +11.+- Top: from the chamfer end (−7, 18) horizontal to (0, 18) (the 10° line starts on the hole's vertical centreline,+ not at the chamfer; this is what the key requires), then a line falling at 10° to the right, tangent into a concave+ R19 arc ending at P = (A−43, 24) = (38, 24), the sharp top-left corner of the right lobe. The R19 arc is defined by+ R19 + tangent to the 10° line + through P (centre (21.54, 33.49); tangent point (18.24, 14.78)).+- Right lobe: top flat y = 24 from P to (A−19, 24); 45° chamfer 5×5; right edge x = A−14 = 67 from y = 19 down to+ y = 0 (corner at hole-centre height = B−57).+- Flank: straight line from (A−14, 0) to (A−43, −33) (48.7°, defined by its end points).+- V bottom flat: y = −33 (19 below the lobe-bottom line) from x = 38 leftward to where the R29 arc meets it (x = 32.79).+- R29 concave arc, centre at the intersection of the flank line (extended) with the lobe-bottom line y = −14 →+ (54.70, −14), running from the V flat up to an R5 fillet tangent to R29 and to the bottom line (fillet centre+ (21.07, −19)).+- Bottom line y = −14 from the fillet back to (−7, −14), then the chamfer.++Profile area 2765.79 mm² (pixel-measured 2766 ± 3). Features: one sketch (lines, two tangent arcs R19/R29, R5+fillet, 45° chamfers, circle) → Boss-Extrude C → material.++## Part 2 — Tool Block, Step 2 (official, Q4)++Key: **1032.32 g**. Rebuilt: 1032.32 g (0.00 %). A = 84, B = 59, C = 45, "all other dimensions the same".+What floats (the only hypothesis that reproduces the key AND the hole callout): the 24 (top → right-edge corner),+14 (hole → lobe bottom), 19 (V depth), 29, 14 hole offset, chamfers and radii are fixed; the right-lobe top is at+y = B−33 = 26, the right-edge corner at y = B−57 = 2, P = (A−43, 26) = (41, 26); the flank runs (A−14, 2)→(A−43, −33);+R29 centre again flank∩(y = −14). (Alternative "top = 26, corner still at y = 0" gives 1036.19 g, +0.37 %.)++## Part 3 — Tool Block, Step 3 (official, Q5; p.21–22)++Key: **628.18 g** (volume 79,516.5 mm³). Rebuilt: 628.18 g (0.000 %). A = 86, B = 58, C = 44 → top y = 25,+corner y = 1, P = (43, 25), right edge x = 72, flank (72, 1)→(43, −33). Features after the Step 2 part (mass is+order-independent):+1. Reference plane offset 12 from the back face; Cut-Extrude on it, blind 24 toward the front (slot at+ z = 12…36 of 44): sketch = circle R41 (Ø82) centred at (A−14, 28.70) = (72, 28.70). The drawing gives R41 and+ "36" = vertical distance from the top edge down to where the arc meets the flank; the intent that reproduces the+ key exactly is "centre on the extension of the right edge, arc through the flank point 36 below the top+ (61.76, −11)". Sensitivity: centre (72, 25) → −3.1 %; (72, 28.0) → −0.6 %.+2. Cut-Extrude from the front face, blind 19, through all in x: rectangle covering everything below the+ lobe-bottom line (y ≤ −14). Shown only in the side view on p.22 and the third isometric; omitting it gives +8.6 %.+3. Cut-Extrude from the front face, blind 19: rectangle from the left edge to the vertical line 52 from the right+ edge (hole-x ≤ 20), all y. Leaves the left lobe 25 thick at the back.+4. Ø11 hole through all, centred 14 from the right edge and 14 below the top edge → (58, 11); goes through both+ ears (back 12 + front 8).+Removed: step 1003.7 mm² × 19, under-notch 358.8 × 19, slot 1015.0 × 24, Ø14 × 25, Ø11 × 20.++## Part 4 — Tool Block, Step 4 (official, Q6; p.24)++Key: **432.58 g** (volume 54,757 mm³). "Only one pocket on one side; not symmetrical; unshown dims as in Q5."+Rebuilt: 432.86 g (+0.07 %) best case; every plausible reading lands within ±1 %. Two independent video+walkthroughs reach exactly 432.58 with this recipe (two cuts in practice):+- Sketch on the front face of the right section (x ≥ 34 from the left edge): Convert Entities of the face's outer+ loop, Offset Entities 1 mm inward ("1 TYP"): left wall along the step line (x = 35), top along the 10°/R19+ saddle, top flat, 5 chamfer, right edge, flank; bottom = horizontal line y = −13 (1 above the lobe-bottom line; it+ does not follow the V). Also offset the slot's R41 silhouette by 1 mm so a 1-mm wall remains between pocket and+ slot. Cut-Extrude, end condition "Offset From Surface" = 12 from the back face (depth 32), over the region outside+ the slot arc.+- Second cut over the ear region inside the R41 arc (same 1-mm rim, hole excluded): Cut-Extrude "Offset From+ Surface" = 1 mm from the slot's front wall (depth 7, leaves a 1-mm skin; section U-U on p.24 shows the skin and+ the 12 floor).+Areas for a checker: 1-mm-offset outline 1556.1 mm²; part outside R42 581.3 mm²; inside R40 860.4 / inside R41 936.3 mm².++## Part 5 — 3dengr practice set (COMMUNITY; text geometry, every number re-derived analytically)++Material 6061, 2700 kg/m³ (0.0027 g/mm³), MMGS, corner at origin, +X +Y +Z.+- Q5 block 100 (X) × 20 (Y) × 60 (Z): **324.00 g** (120,000 mm³).+- Q6 + Ø20 through hole along Y at X = 25, Z = 30: **307.04 g** (hole 6,283.19 mm³).+- Q7 CoM of Q6: **X = 51.38, Y = 10.00, Z = 30.00** mm.+- Q8 hole changed to Ø30: **285.83 g**.+- Q9 L-bracket, AISI 1020 0.0079: base 80 (X) × 10 (Y) × 40 (Z) at the origin corner; upright 10 thick (X) at the+ left end, 50 above the base, 40 deep: **410.80 g**, CoM **(26.54, 16.54, 20.00)**.++## Further official candidates (drawings only; NOT rebuilt or verified)++- 2007 Q1 (p.1–2): MMGS, Copper 0.0089, A = 63 (height), B = 50 (depth), C = 100 (length); key **1280 g**.+ Callouts: 100, 63, 50, 40, 40, 20, 15, 15, 10, 35, 20°, 45°, 9, 12, 5, 13, 18, 30, 8, Ø20, Ø10 (×3), Ø50, R25 (×2),+ R20, R10, R30. Multi-sketch, angled cuts, concentric bores; a human must read the drawing.+- 2007 Q6 (p.6): 6061 0.0027, A = 100; key **2040.57 g**. Base 200 × A × 25 with steps 50/37.5/100/25/15/5, boss+ Ø25 with Ø12 through on an R25/20° lug at 40/15, raised block 60/40/30/10, R50 saddle, 30° face, 10-wide slot,+ Ø15 hole with ⌴Ø25 ⌵5 counterbore, R35. Needs human reading.+- Planchard 3-3 (SDC p.3-7/3-8): A = 50, B = 50, C = 120, 6061 0.0027; key **302.40 g, volume 112,000 mm²,+ surface 26,200 mm², CoM (0, 19.20, 0)**; mirrored stepped sketch on Front → Boss-Extrude mid-plane 50 →+ Cut-Extrude through-all of a 30×20 rectangle on the right face. Figure dims 120, 70, 40, 30, 20, 10, 50; the+ reviewer reproduced the volume but not the CoM, so the figure needs a human read. Fills the "mirror" slot.++## Assembly targets (future)++- Official chain-link assembly (2017 PDF p.26–28): 2 long_pins, 3 short_pins, 4 chain_links (parts in the sample+ ZIP on solidworks.com/cswa; not retrieved); pins concentric to link holes, pin end faces coincident to link side+ faces; A = 25°, B = 125°, C = 130° → CoM **(348.66, −88.48, −91.40)**; A = 30°, B = 115°, C = 135° →+ **(327.67, −98.39, −102.91)**.+- 2007 Q4: 3 identical L-brackets + 2 titanium pins, 45° angle mates → CoM option c (40.24, 24.33, 20.75).+ 2007 Q5: Base / Yoke / AdjustingPin, 1060 Alloy → option d (30.00, 40.16, −53.82). Drawings p.4–5 (image only).++## Ambiguities and unverifiable items++1. Tool Block drawings are images; the start of the 10° line (hole centreline, not the chamfer) is not called out;+ pixel measurement + the key fix it.+2. Step 2: which dimension floats with B is not stated; the mass key fixes it (corner at y = B−57), but the+ hole-height alternative gives the same mass; a CoM check would settle it.+3. Step 3: the R41 centre is not dimensioned (see the intent above; the key is reproduced exactly). The 19×19+ under-notch cut is shown only in the side view.+4. Step 4: the wall between pocket and slot (1 vs 2 mm), whether the Ø11 keeps a 1-mm ring, and whether the ear cut+ leaves a skin cannot be read from the PDF; all combinations fall within −0.8…+1.0 % of the key, so this part must+ use the exam's 1 % band.+5. The current solidworks.com sample ZIP (chain-link part files) was not retrieved.+6. Goodheart-Willcox practice set has no published key; excluded.+7. 2007 Q1/Q6 and Planchard 3-3: dimensions only in images; not transcribed or verified.
tools/dsl-live-check.py+9−4@@ -274,7 +274,7 @@ def union_bbox(bodies): def run_check(cid, title, issue, script, exp): rec = {"id": cid, "title": title, "issue": issue, "expect": exp, "fails": []} t0 = time.time()- req = {"script": script, "reason": f"dsl-live-check {cid}: {title}"}+ req = {"script": script, "purpose": "bridge", "reason": f"dsl-live-check {cid}: {title}"} if REUSE["path"] and cid not in FRESH_ALWAYS and script and script[0].get("op") == "new_part": req["reuseDocument"] = REUSE["path"] rec["reused"] = True@@ -303,6 +303,8 @@ def run_check(cid, title, issue, script, exp): if not ok(r): rec["fails"].append(f"swx_run_script failed: {r.get('errorCode')} {str(r.get('error'))[:300]}") rec["seconds"] = round(time.time() - t0, 1)+ if r.get("errorCode") in ("user_present", "low_commit_memory", "stopped_by_dialog"):+ rec["stopBattery"] = r.get("errorCode") return rec for st in steps: res = st.get("result") or {}@@ -387,7 +389,7 @@ def run_check(cid, title, issue, script, exp): def ownership_check(): """#2.b: a part made by swx_new_document, then scripted by PATH, is the bridge's own.""" rec = {"id": "T23", "title": "issue #2: run_script on the bridge's own new part (addressed by path) says bridgeCreatedDocument:true", "issue": "#2.b", "fails": []}- nd = call("swx_new_document", {"type": "part", "reason": "dsl-live-check T23"})+ nd = call("swx_new_document", {"type": "part", "purpose": "bridge", "reason": "dsl-live-check T23"}) rec["newDocument"] = nd path = (nd.get("created") or {}).get("path") or (nd.get("documentIdentity") or {}).get("path") or nd.get("path") title = (nd.get("created") or {}).get("title") or nd.get("title")@@ -426,7 +428,7 @@ def main(): results = [] plan = checks() if a.reuse:- nd = call("swx_new_document", {"type": "part", "reason": "dsl-live-check --reuse: the one part every check that allows it runs in"})+ nd = call("swx_new_document", {"type": "part", "purpose": "bridge", "reason": "dsl-live-check --reuse: the one part every check that allows it runs in"}) REUSE["path"] = (nd.get("documentIdentity") or {}).get("path") or (nd.get("created") or {}).get("path") print(f"reuse part: {'created' if REUSE['path'] else 'FAILED (' + str(nd.get('errorCode')) + '), running with fresh parts'}") for cid, title, issue, script, exp in plan:@@ -437,6 +439,9 @@ def main(): print(f"{'PASS' if not rec['fails'] else 'FAIL'} {cid} {title} ({rec.get('seconds')} s)") for f in rec["fails"]: print(f" - {f}")+ if rec.get("stopBattery"):+ print(f"STOP the bridge refused to create ({rec['stopBattery']}); no more parts will be created. Re-run when the user is idle / memory is back / the dialog is answered.")+ break if not a.keep and not rec.get("reused"): doc = (rec.get("runScript") or {}).get("documentName") if REUSE["path"] and doc:@@ -446,7 +451,7 @@ def main(): rw = call("swx_close_doc", {"targetDocument": doc, "save": False, "reason": f"dsl-live-check: close the fresh part of {cid} (reuse mode keeps the shared part)"}) if not ok(rw): rw = call("swx_reset_workspace", {"reason": f"dsl-live-check: close the scratch part of {cid} (close_doc refused: {rw.get('errorCode')})"})- nd = call("swx_new_document", {"type": "part", "reason": "dsl-live-check --reuse: re-create the shared part after a workspace reset"})+ nd = call("swx_new_document", {"type": "part", "purpose": "bridge", "reason": "dsl-live-check --reuse: re-create the shared part after a workspace reset"}) REUSE["path"] = (nd.get("documentIdentity") or {}).get("path") or (nd.get("created") or {}).get("path") else: rw = call("swx_reset_workspace", {"reason": f"dsl-live-check: close the scratch part of {cid}"})
uninstall.sh