← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.10.0+0.10.1
Cargo.lock+1−1
@@ -122,7 +122,7 @@ dependencies = [  [[package]] name = "swx-bridge"-version = "0.10.0"+version = "0.10.1" dependencies = [  "miniz_oxide",  "serde",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.0"+version = "0.10.1" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+15−1
@@ -492,7 +492,21 @@ not.  ## Roadmap -- v0.10.0 (this): **issue #1, the modelling DSL Michael asked for, and+- v0.10.1 (this): **the self-test never reports a closed SOLIDWORKS as a failure,+  and a Connected install is not called classic.** Measured on hosted 0.10.0+  with SOLIDWORKS closed: `swx_selftest` counted `swx_docs` and+  `swx_get_preferences` as failures ("SOLIDWORKS is not running"). Now:+  - every check that needs SOLIDWORKS is skipped, with the reason, and the+    summary says `solidworksRunning: false`;+  - the hint's first sentence asks for SOLIDWORKS to be started;+  - `level:"write"` is refused up front and changes nothing.++  `swx_status.solidworks.classicYears` said `[2026]` on the 3DEXPERIENCE 2026+  install. `registryYears` now carries the raw registry years, and+  `classicYears` only the years with no 3DEXPERIENCE entry or install path for+  that release, which is `[]` on that machine. The edition decision is+  unchanged. 576 tests.+- v0.10.0: **issue #1, the modelling DSL Michael asked for, and   SOLIDWORKS 2021 as a supported release.**   - **Sketches.** One `sketch` holds several profiles, so concentric circles are     a ring. A sketch can go on an offset plane
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "solidworks",   "displayName": "SOLIDWORKS",-  "version": "0.10.0",+  "version": "0.10.1",   "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/COMPATIBILITY.md+12−1
@@ -40,7 +40,7 @@ only ever **run** against **3DEXPERIENCE SOLIDWORKS 2026** (Makers, revision  | | 3DEXPERIENCE SOLIDWORKS (2026, measured) | Classic desktop (2021, unmeasured) | |---|---|---|-| 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.classicYears` lists the years. |+| 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` | | Launch | The Platform's own shortcut (CATSTART, then SWXDesktopLauncher), never the exe | `CoCreateInstance("SldWorks.Application")`, which starts `SLDWORKS.exe` itself. With several releases installed, set `SWX_PROGID=SldWorks.Application.29` to pick 2021 [unmeasured]. | | Sign-in | 3DEXPERIENCE ID (three measured hosts) | None. The licence is standalone or SolidNetWork; the bridge never sees a licence prompt and never answers one. |@@ -258,6 +258,17 @@ Each row is `{name, verb, pass, ms, errorCode, error, hostError}`. The the normal dispatcher, readiness gate included. The self-test is itself exempt from the gate, so a check held at the gate is reported, not hidden. +**Run it with SOLIDWORKS started.** With SOLIDWORKS closed (measured+2026-10-13), only `swx_status` and `swx_readiness` can run:+- every check that needs SOLIDWORKS is `skipped`, with the reason, and is never+  counted as a failure;+- the summary carries `solidworksRunning: false`;+- the `_hint` begins "Start SOLIDWORKS (swx_start, or by hand) and run this+  again".++`level:"write"` with SOLIDWORKS closed is refused up front+(`solidworks_not_running`) and changes nothing.+ ## What is still unmeasured on 2021  - The classic launch route (`CoCreateInstance`) and the `Setup\SolidWorks
install.sh

  
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "solidworks-bridge",-  "version": "0.10.0",+  "version": "0.10.1",   "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": [
src/detect.rs+78−7
@@ -46,7 +46,8 @@ impl Detection {             // CLASSIC DESKTOP RELEASES in the registry (0.10.0): SOLIDWORKS 2021             // is major version 29. Launched through COM (the exe), with no             // 3DEXPERIENCE sign-in; see docs/COMPATIBILITY.md.-            "classicYears": classic_years(&self.registry_versions),+            "registryYears": registry_years(&self.registry_versions),+            "classicYears": classic_years_from(self.exe.as_deref(), &self.registry_versions, edition),         })     } @@ -119,7 +120,7 @@ pub fn edition_from(     // `HKLM\SOFTWARE\SolidWorks\SOLIDWORKS 2021`, a year and nothing else, with no     // 3DEXPERIENCE marker anywhere (checked above) and an exe found. UNMEASURED     // on a classic machine; it is the documented layout of the desktop installer.-    if exe.is_some() && !classic_years(registry_versions).is_empty() {+    if exe.is_some() && !registry_years(registry_versions).is_empty() {         return "desktop";     }     if exe.is_none() {@@ -131,9 +132,11 @@ pub fn edition_from(     "unknown" } -/// The classic desktop releases named in the registry: "SOLIDWORKS 2021" ->-/// 2021. A 3DEXPERIENCE entry ("SOLIDWORKS 3DEXPERIENCE R2026x") is not one.-pub fn classic_years(registry_versions: &[String]) -> Vec<u32> {+/// Every release year a plain `SOLIDWORKS <year>` registry key names:+/// "SOLIDWORKS 2021" -> 2021. A FACT about the registry, not a claim about the+/// edition — MEASURED 2026-10-13: the 3DEXPERIENCE (Connected) 2026 install+/// writes "SOLIDWORKS 2026" too, so this alone is not "a classic install".+pub fn registry_years(registry_versions: &[String]) -> Vec<u32> {     let mut out: Vec<u32> = registry_versions         .iter()         .filter_map(|v| {@@ -147,6 +150,63 @@ pub fn classic_years(registry_versions: &[String]) -> Vec<u32> {     out } +/// The release years tied to 3DEXPERIENCE / Connected: a registry entry or an+/// install path that names the platform and a year ("SOLIDWORKS 3DEXPERIENCE+/// R2026x" -> 2026).+pub fn connected_years(exe: Option<&str>, registry_versions: &[String]) -> Vec<u32> {+    fn years_in(s: &str) -> Vec<u32> {+        let b = s.as_bytes();+        let mut out = Vec::new();+        let mut i = 0;+        while i + 4 <= b.len() {+            let run = &b[i..i + 4];+            let boundary_before = i == 0 || !b[i - 1].is_ascii_digit();+            let boundary_after = i + 4 == b.len() || !b[i + 4].is_ascii_digit();+            if run.iter().all(|c| c.is_ascii_digit()) && boundary_before && boundary_after {+                if let Ok(y) = std::str::from_utf8(run).unwrap_or("").parse::<u32>() {+                    if (2000..2100).contains(&y) {+                        out.push(y);+                    }+                }+                i += 4;+            } else {+                i += 1;+            }+        }+        out+    }+    let marked = |t: &str| {+        let u = t.to_uppercase();+        u.contains("3DEXPERIENCE") || u.contains("CONNECTED")+    };+    let mut out: Vec<u32> = registry_versions.iter().filter(|v| marked(v)).flat_map(|v| years_in(v)).collect();+    if let Some(e) = exe.filter(|e| marked(e)) {+        out.extend(years_in(e));+    }+    out.sort();+    out.dedup();+    out+}++/// The CLASSIC desktop releases on this machine: registry years with no+/// 3DEXPERIENCE entry or install path for the same release. On a Connected+/// machine whose platform marker names no year (the launcher alone), no year+/// can be shown to be classic, so none is claimed. MEASURED 2026-10-13: 0.10.0+/// reported `classicYears: [2026]` on the 3DEXPERIENCE 2026 install, which+/// read as a classic install that is not there. The EDITION decision is not+/// made here and did not change.+pub fn classic_years_from(exe: Option<&str>, registry_versions: &[String], edition: &str) -> Vec<u32> {+    let all = registry_years(registry_versions);+    if edition == "desktop" {+        return all;+    }+    let connected = connected_years(exe, registry_versions);+    if edition == "connected" && connected.is_empty() {+        return Vec::new();+    }+    all.into_iter().filter(|y| !connected.contains(y)).collect()+}+ /// How this bridge may start SOLIDWORKS. /// /// LIVE FINDING (2026-09-03) — THE LAUNCH ANSWER. The earlier conclusion that@@ -541,7 +601,7 @@ mod windows_impl {     /// <year>\Setup` value "SolidWorks Folder" (newest year first). UNMEASURED.     fn classic_setup_exe(versions: &[String]) -> Option<String> {         let hive = RegKey::predef(HKEY_LOCAL_MACHINE);-        let mut years = super::classic_years(versions);+        let mut years = super::classic_years_from(None, versions, "unknown");         years.reverse();         for y in years {             if let Ok(key) = hive.open_subkey(format!(r"SOFTWARE\SolidWorks\SOLIDWORKS {y}\Setup")) {@@ -725,7 +785,18 @@ mod tests {         assert_eq!(edition_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, false), "desktop");         assert_eq!(edition_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, true), "connected");         assert_eq!(edition_from(None, &y2021, false), "unknown", "no exe: nothing to launch");-        assert_eq!(classic_years(&["SOLIDWORKS 2021".into(), "SOLIDWORKS 3DEXPERIENCE R2026x".into(), "SOLIDWORKS 2019".into()]), vec![2019, 2021]);+        assert_eq!(registry_years(&["SOLIDWORKS 2021".into(), "SOLIDWORKS 3DEXPERIENCE R2026x".into(), "SOLIDWORKS 2019".into()]), vec![2019, 2021]);+        // classicYears (0.10.1): MEASURED 2026-10-13, the 3DEXPERIENCE 2026 install+        // also writes "SOLIDWORKS 2026" — that year is not a classic install.+        let x2026 = r"C:\Program Files\Dassault Systemes\SOLIDWORKS 3DEXPERIENCE R2026x\SOLIDWORKS\SLDWORKS.exe";+        let reg26 = vec!["SOLIDWORKS 2026".to_string()];+        assert_eq!(edition_from(Some(x2026), &reg26, true), "connected", "the edition decision is unchanged");+        assert_eq!(registry_years(&reg26), vec![2026]);+        assert!(classic_years_from(Some(x2026), &reg26, "connected").is_empty(), "the Connected release is not classic");+        assert!(classic_years_from(None, &reg26, "connected").is_empty(), "a marker naming no year: no year can be shown to be classic");+        let mixed = vec!["SOLIDWORKS 2021".to_string(), "SOLIDWORKS 2026".to_string(), "SOLIDWORKS 3DEXPERIENCE R2026x".to_string()];+        assert_eq!(classic_years_from(None, &mixed, "connected"), vec![2021], "a classic 2021 beside a Connected 2026");+        assert_eq!(classic_years_from(Some(r"D:\SW2021\SLDWORKS.exe"), &y2021, "desktop"), vec![2021]);         assert_eq!(edition_from(None, &none, false), "unknown");     } 
src/selftest.rs+59−2
@@ -84,12 +84,56 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {         "edition": sw.get("edition"),         "launchRoute": sw.get("launchRoute"),         "versionSupport": sw.get("versionSupport"),+        "registryYears": sw.get("registryYears"),         "classicYears": sw.get("classicYears"),         "registryVersions": sw.get("registryVersions"),     }));     let readiness = run.call(ctx, "readiness", "swx_readiness", json!({}));     let ready = readiness.get("ready").and_then(|v| v.as_bool()) == Some(true)         || readiness.pointer("/solidworks/ready").and_then(|v| v.as_bool()) == Some(true);+    // IS SOLIDWORKS THERE TO TEST? MEASURED 2026-10-13 (hosted 0.10.0, SOLIDWORKS+    // closed): swx_docs and swx_get_preferences were reported as FAILURES with+    // "SOLIDWORKS is not running" — not a verb failing on this release, and a+    // user who ran the self-test before starting SOLIDWORKS would have sent us+    // false failures. So: every check that needs SOLIDWORKS is SKIPPED (why+    // said) when COM is not attached; status and readiness still run. A COM+    // host busy with a long verb is "cannot tell", and the checks run.+    let com = status.get("com").cloned().unwrap_or(Value::Null);+    let running = com.get("running").and_then(|v| v.as_bool()).or_else(|| com.get("attached").and_then(|v| v.as_bool()))+        .unwrap_or(com.get("busy").and_then(|v| v.as_bool()) == Some(true));+    if !running {+        let process = sw.get("running").and_then(|v| v.as_bool()) == Some(true);+        let why = if process {+            "SOLIDWORKS is running but this bridge is not attached to it yet (still starting, or waiting on a sign-in)"+        } else {+            "SOLIDWORKS is not running"+        };+        let ran = run.results.len();+        let start_hint = format!(+            "Start SOLIDWORKS (swx_start, or by hand) and run this again — with it closed only {ran} checks can run."+        );+        if level == "write" {+            // Refused UP FRONT: nothing was created or changed.+            let mut out = crate::verbs::err_code(+                "solidworks_not_running",+                format!("swx_selftest level:\"write\" needs SOLIDWORKS: {why}. Nothing was created or changed."),+                start_hint,+            );+            if let Value::Object(m) = &mut out {+                m.insert("results".into(), Value::Array(run.results));+                m.insert("solidworksRunning".into(), json!(false));+            }+            return out;+        }+        for (name, verb) in [+            ("document list", "swx_docs"),+            ("preferences", "swx_get_preferences"),+            ("the active document's read verbs", "swx_design_info"),+        ] {+            run.skip(name, verb, why);+        }+        return finish(level, &sw, ready, false, started, run, Some(start_hint));+    }     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);@@ -210,9 +254,20 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {         }     } +    finish(level, &sw, ready, true, started, run, None)+}++/// The reply: the rows, the counts, and — when SOLIDWORKS was not there —+/// that FIRST, so nobody reads a skipped check as a failure.+fn finish(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>) -> Value {     let passed = run.results.iter().filter(|r| r["pass"] == true).count();     let failed = run.results.iter().filter(|r| r["pass"] == false).count();     let skipped = run.results.iter().filter(|r| r["pass"].is_null()).count();+    let send = "Send this WHOLE reply back (for example on the bridge's wiki issue): each row is one verb or one modelling op on your install, with the error SOLIDWORKS gave when it failed. level:\"read\" changed nothing; level:\"write\" built, exported, saved and closed a scratch part in the bridge's own scratch folder and re-activated the document that was active before.";+    let hint = match lead {+        Some(first) => format!("{first} {send}"),+        None => send.to_string(),+    };     crate::verbs::ok(json!({         "level": level,         "bridgeVersion": crate::BRIDGE_VERSION,@@ -220,11 +275,13 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {             "versionSupport": sw.get("versionSupport"),             "edition": sw.get("edition"),             "launchRoute": sw.get("launchRoute"),+            "registryYears": sw.get("registryYears"),             "classicYears": sw.get("classicYears"),             "ready": ready,+            "running": running,         },-        "summary": {"passed": passed, "failed": failed, "skipped": skipped, "seconds": started.elapsed().as_secs_f64()},+        "summary": {"passed": passed, "failed": failed, "skipped": skipped, "solidworksRunning": running, "seconds": started.elapsed().as_secs_f64()},         "results": run.results,-        "_hint": "Send this WHOLE reply back (for example on the bridge's wiki issue): each row is one verb or one modelling op on your install, with the error SOLIDWORKS gave when it failed. level:\"read\" changed nothing; level:\"write\" built, exported, saved and closed a scratch part in the bridge's own scratch folder and re-activated the document that was active before.",+        "_hint": hint,     })) }
tests/issue1.rs+42
@@ -217,3 +217,45 @@ fn selftest_reports_every_check_and_cleans_up_after_itself() {     let bad = b.call("swx_selftest", json!({"level": "everything"}));     check!(b, "an unknown level is refused", bad["errorCode"] == "bad_level", s(&bad)); }++/// MEASURED 2026-10-13 (hosted 0.10.0, SOLIDWORKS closed): the self-test+/// reported swx_docs and swx_get_preferences as FAILURES ("SOLIDWORKS is not+/// running") — false failures a user would have sent back.+#[test]+fn selftest_with_solidworks_closed_skips_instead_of_failing_and_write_refuses() {+    let (b, _m) = boot("issue1-selftest-closed");+    let r = b.call("swx_selftest", json!({}));+    let rows = r["results"].as_array().cloned().unwrap_or_default();+    let skipped_why = |verb: &str| rows.iter().any(|x| x["verb"] == verb && x["pass"].is_null() && x["skipped"] == "SOLIDWORKS is not running");+    check!(+        b,+        "read level, SOLIDWORKS closed: nothing FAILED; docs/preferences/active-document reads are SKIPPED, why said; solidworksRunning:false",+        r["success"] == true && r["summary"]["failed"] == 0 && r["summary"]["solidworksRunning"] == false+            && r["summary"]["passed"] == 2 && skipped_why("swx_docs") && skipped_why("swx_get_preferences") && skipped_why("swx_design_info"),+        s(&r)+    );+    check!(+        b,+        "the hint's FIRST sentence says to start SOLIDWORKS and how many checks could run",+        r["_hint"].as_str().unwrap_or("").starts_with("Start SOLIDWORKS (swx_start, or by hand) and run this again — with it closed only 2 checks can run."),+        s(&r["_hint"])+    );+    let w = b.call("swx_selftest", json!({"level": "write"}));+    check!(+        b,+        "write level, SOLIDWORKS closed: refused UP FRONT with the same advice",+        w["success"] == false && w["errorCode"] == "solidworks_not_running"+            && w["_hint"].as_str().unwrap_or("").starts_with("Start SOLIDWORKS") && w["solidworksRunning"] == false,+        s(&w)+    );+    b.call("swx_start", json!({}));+    let docs = b.call("swx_docs", json!({}));+    let ledger = b.call("swx_action_ledger", json!({"limit": 200}));+    check!(+        b,+        "...and changed nothing: no document was created",+        docs["count"] == 0+            && !ledger["actions"].as_array().map(|a| a.iter().any(|e| e["action"] == "document_created")).unwrap_or(false),+        format!("{}\n{}", s(&docs), s(&ledger))+    );+}
uninstall.sh