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.7 source
23 files changed
+940−304
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+35−1bridge.json+1−1docs/LIVE-RESULTS.md+12install.shpackage.json+1−1references/modelling-and-export.md+9−14skills/solidworks-memory/SKILL.md+48−39src/comhost/com.rs+25−4src/comhost/mock.rs+22−2src/compat.rs+10−4src/launch.rs+201−1src/memory.rs+85−36src/script.rs+47src/selftest.rs+68−21src/verbs.rs+14−3src/verbs3.rs+35−11tests/memory.rs+90tests/quit.rs+8tests/visibility.rs+226−163uninstall.shBRIDGE_VERSION+1−1@@ -1 +1 @@-0.10.6+0.10.7
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.10.6"+version = "0.10.7" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.6"+version = "0.10.7" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+35−1@@ -498,7 +498,41 @@ not. ## Roadmap -- v0.10.6 (this): **fixing the memory growth at its root.** Measured+- v0.10.7 (this): **what the live run decided.** 0.10.6 was measured on+ SOLIDWORKS 2026:+ - Hidden documents cannot be modelled. SOLIDWORKS selects only in the active+ document, and a hidden one cannot be activated.+ - Twelve builds in ONE reused, visible part left SOLIDWORKS flat (3260 to+ 3246 MB, handles 4373 to 4398). Each new part costs about 85 MB and 125+ handles.+ - `swx_com_respawn` freed about 210 MB while the handles did not drop, so+ the bridge does not pin documents.+ - The Connected edition's launcher re-parents, so 0.10.6 never recognised its+ own launch.++ So this release:+ - **Visible by default.** The self-test, the quit ladder's scratch part,+ `swx_new_document` and scripts are all visible. A script that would model a+ hidden part is refused up front with `document_hidden`.+ - **One visible reused document per battery.** The self-test uses it,+ reports `documentsCreated`, and ends by respawning the COM host+ (`comRespawn.freedMb`). A `reuseDocument` that cannot be used says so by+ name. Measured, it read "needs a document", because the host looked only at+ the active document.+ - **Attribution at attach.** First ancestry; failing that, the ONE+ sldworks.exe that was not in the launch's baseline and was created between+ the attempt's start and the attach. Never with two candidates, never a+ process other than COM's. Recorded as+ `attributedSolidworks.how:"newProcessInLaunchWindow"`.+ - **No throttling.** The self-test carries on when commit is high but+ SOLIDWORKS is within its budget, and reports it (`memoryPressureSeen`). It+ recycles the bridge's own empty session when SOLIDWORKS itself is over+ budget. It stops only at critical, or when SOLIDWORKS is over budget in a+ session it may not end. The verb guard is unchanged (critical only).+ - `clear_part` is now marked measured live.++ 628 tests.+- v0.10.6: **fixing the memory growth at its root.** Measured 2026-10-14 on 0.10.5 (12 parts built and closed, on a fresh SOLIDWORKS 2026): SOLIDWORKS's private bytes rose 520 MB, 913, 1191, then +83 MB per part to 2035 MB. Handles went 2845 to 4539 (about +125 per part).
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.10.6",+ "version": "0.10.7", "description": "Bridge for SOLIDWORKS desktop CAD — create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.", "author": "Adom Inc.", "license": "MIT",
docs/LIVE-RESULTS.md+12@@ -78,6 +78,18 @@ and projects the origin onto the face, so their circles were moved to sketch | T39 | a linear pattern in two directions (X +12, Z -12): 4 bosses on the block | #1.6 | 4084.823 | 4084.823 | [-10, 0, -10] to [10, 13, 10] | PASS | | T42 | a shell outward 1 mm with the top face removed: the block becomes the cavity | #1.6 | 1324.0 | 1324.0 | [-11, -1, -11] to [11, 10, 11] | PASS | +## Memory, measured 2026-10-14 (bridges 0.10.5 and 0.10.6)++| What | Measured |+|---|---|+| A new part per build (12 builds, each closed) | SOLIDWORKS private bytes +83 to +85 MB per part (520 to 2035 MB; again 538 to 1697 MB over 8); handles +~125 per part; `GetDocumentCount` 0 after every close |+| ONE reused part (`reuseDocument`, 12 builds) | 3260 to 3246 MB, handles 4373 to 4398: flat |+| A part created hidden (`visible:false`), then modelled | refused by SOLIDWORKS: a hidden document cannot be activated, and modelling selects |+| `swx_com_respawn` | about 210 MB freed (1697 to 1487 MB), handles unchanged (4022 to 4056): the bridge does not pin documents |+| Creating the reuse part (once) | +1.95 GB while open (1487 to 3437 MB), +285 MB after its close — observed, unexplained |++`clear_part` (the reuse step) is marked measured on 2026 in the variants table.+ ## Not yet measured - SOLIDWORKS **2021** (and any version other than 2026): supported by the API audit in `docs/COMPATIBILITY.md`, not yet run. `swx_selftest {"level":"write"}` on that install answers it.
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.10.6",+ "version": "0.10.7", "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+9−14@@ -40,20 +40,15 @@ adom-bridge --ai-thread "<thread>" swx_save_as '{"outputPath":"C:\\parts\\bracke - `swx_new_document` gives you an **unsaved** document: it has a title (`Part1`) and **no path** until `swx_save_as`. Target it by `name`, not by path.-- **Hidden documents and reuse (0.10.6).** Measured 2026-10-14: every document created- and closed left SOLIDWORKS about 83 MB and 125 handles heavier, for the life of the- session.- - Make scratch and test geometry hidden with `swx_new_document {"type":"part",- "visible":false}`, or `{"op":"new_part","visible":false}` in a script. The document- has no window, and every object-model verb works on it. SOLIDWORKS cannot show a- hidden-created document later, so save it and reopen it to see it.- - A user's own script stays visible by default.- - For a batch, run each script with `"reuseDocument": "<path of a part the bridge- made>"`. Its leading `new_part` becomes `clear_part`, which deletes every feature- after the origin, and only in a part the bridge created. N checks then cost one- document.- - Whether hiding or reuse stops the growth is UNMEASURED. See the- **solidworks-memory** skill.+- **Batches reuse one document (measured, 2026-10-14).**+ - Every NEW part costs SOLIDWORKS about 85 MB and 125 handles, kept until it restarts.+ Twelve builds in ONE reused part left it flat.+ - Make one visible part with `swx_new_document`, then run each script with+ `"reuseDocument": "<its path>"`. Its leading `new_part` becomes `clear_part`, which+ deletes every feature after the origin, and only in a part the bridge created.+ - A hidden document (`visible:false`) CANNOT be modelled. SOLIDWORKS selects only in the+ active document, and a hidden one cannot be activated, so scripts refuse it+ (`document_hidden`). Everything is visible by default. - `swx_run_script` takes a JSON **array of steps**, not a string of code — SOLIDWORKS has no in-process scripting host the way Fusion has Python. Every dimension is in **millimetres**. A rejected script returns the full op list
skills/solidworks-memory/SKILL.md+48−39@@ -38,21 +38,26 @@ Parent skill: **solidworks-bridge** **520 MB, 913 after part 1, 1191 after part 2, then a straight line of +83 MB per part to 2035 MB after 12**. Its handle count went **2845 to 4539, about +125 per part**, also linear. `GetDocumentCount` was **0** after every close: no hidden documents linger.-- [measured] So every scratch document created and closed costs about **83 MB and 125- handles for good**, for the life of the session.-- [unmeasured] The leading explanation: each new VISIBLE document gets a window and its- own graphics context and driver resources, which SOLIDWORKS does not give back on close.- SOLIDWORKS's own help for `IModelDoc2::Visible` says hiding a document does not release- its user-interface resources, and points to opening documents invisibly with- `ISldWorks::DocumentVisible` instead.-- [unmeasured] The other candidate is a COM reference the bridge itself keeps. A code- review found none. `swx_com_respawn {"reason": ...}` settles it: it restarts only the- bridge's own COM host, releasing every reference the bridge holds, and never calls- SOLIDWORKS. If SOLIDWORKS's handle count drops after a respawn (allow COM's roughly- six-minute ping rundown if the old host had to be killed), the bridge was pinning- something.-- [unmeasured] Whether hidden documents or a reused document stop the growth is not yet- measured. Bridge 0.10.6 makes both possible; the next live run answers it.+ Re-measured with 0.10.6: 538 to 1697 MB over 8 new parts (+85 MB and +125 handles each).+- [measured] **Reusing ONE document is the fix.** Bridge 0.10.6: one visible part made+ with `swx_new_document`, then 12 builds with `swx_run_script {"reuseDocument": <its path>}`+ (each build's `new_part` becomes `clear_part`). Private bytes went **3260 to 3246 MB**,+ handles **4373 to 4398**, over all 12 builds: no growth at all.+- [measured] **Hidden documents cannot be modelled.** A part created with+ `visible:false` (`ISldWorks::DocumentVisible`) cannot be activated: modelling it failed+ "could not activate 'Part1'". SOLIDWORKS selects only in the active document, and every+ modelling step selects something (a plane, a sketch, an edge, a face), so hiding is not+ a memory fix for modelling. From 0.10.7 the bridge refuses that up front+ (`document_hidden`) and makes everything visible by default.+- [measured] **The bridge does not pin the documents.** `swx_com_respawn` (it restarts+ only the bridge's COM host, releasing every reference it holds, and never calls+ SOLIDWORKS) freed about 210 MB of private bytes (1697 to 1487 MB). The handle count did+ not drop (4022 to 4056). So the per-document cost is SOLIDWORKS's own, kept until+ SOLIDWORKS restarts.+- [measured, unexplained] Creating the reuse part once raised SOLIDWORKS's private bytes+ from 1487 to 3437 MB (+1.95 GB) while it was open. After it was closed, SOLIDWORKS held+ 1772 MB, 285 MB more than before it was created. This happened once and has no+ explanation yet; record it if you see it again. ## The rules @@ -60,12 +65,17 @@ Parent skill: **solidworks-bridge** `memory` block (commit used, limit and available; SOLIDWORKS's own committed bytes and handles; the largest holders). [measured: SOLIDWORKS's private bytes and handle count, read through the 12-part run on 2026-10-14] [unmeasured: the pressure verdict and the- largest-holders list] If commit available is low before you start, do not start: tell- the user what holds the memory.-2. **Recycle a session the bridge started, never one the user is working in.** When- SOLIDWORKS's own commit passes a budget (6 GB is a sensible default on a 32 GB PC) or- commit available drops under about 10 % of the limit, and the session was started by- the bridge and has **zero documents open**: `swx_stop {"userAuthorized": true, "reason": ...}`+ largest-holders list] If commit is CRITICAL before you start, do not start: tell the+ user what holds the memory. HIGH pressure held by other applications is not a reason to+ stop a run that reuses one document: it keeps SOLIDWORKS flat (measured).+2. **Recycle a session the bridge started, never one the user is working in.**+ [measured] On the 3DEXPERIENCE edition the launcher chain re-parents, so 0.10.6 never+ recognised its own launch (`attributedSolidworks` was null). [unmeasured] From 0.10.7+ the bridge claims the one sldworks.exe that appeared in the launch window+ (`swx_status.launch.attributedSolidworks.how`). When SOLIDWORKS's OWN commit passes a+ budget (6 GB is a sensible default on a 32 GB PC), or commit is critical and SOLIDWORKS+ holds a real share of it, and the session was started by the bridge and has **zero+ documents open**: `swx_stop {"userAuthorized": true, "reason": ...}` then `swx_start` (1-3 minutes on a 3DEXPERIENCE edition). `swx_stop` refuses on its own when documents are open or the user is active; never work around that refusal. 3. **Close what you made as you go.** `swx_reset_workspace` after each scratch document, or@@ -78,28 +88,27 @@ Parent skill: **solidworks-bridge** (The bridge's hidden creation flips one session setting, `DocumentVisible`, for one `NewDocument` call and puts it back straight away, even when the creation fails. It never leaves it changed, and it ledgers both the flip and the restore.)-6. **Split long runs.** A battery of more than about 30 new parts should recycle the- bridge's session between batches. [unmeasured] `swx_selftest {"level":"write"}` does- this itself from 0.10.5: before each check it reads memory, recycles a session the- bridge started with nothing open (its reply lists `recycled`), and otherwise stops with- `stoppedFor:"memory"` rather than end a session that is not its own.+6. **Batch work reuses one document (flat); never one new part per item.** New documents+ cost about 85 MB and 125 handles each until SOLIDWORKS restarts; builds in one reused+ document cost nothing (measured). Make ONE visible part with `swx_new_document`, then+ run each script with `"reuseDocument": "<its path>"`. Its leading `new_part` becomes+ `clear_part`, which deletes every feature after the origin, and only in a part the+ bridge created. `swx_selftest {"level":"write"}` does this itself (it reports+ `documentsCreated`), and it ends by respawning the bridge's COM host+ (`comRespawn.freedMb`). [unmeasured] Between checks, the self-test recycles the+ bridge's own empty session only when SOLIDWORKS ITSELF is over its budget (the+ recycled list says when). It stops only when commit is critical, or when SOLIDWORKS is+ over budget in a session it may not end. High commit held by other applications is+ reported (`memoryPressureSeen`) and does not stop it. 7. **A refusal is not an obstacle to route around.** [unmeasured] While commit is critical, verbs that create or load geometry answer `low_commit_memory` and change nothing; the reply names the holders and the safe remedy. `ignoreMemoryPressure:true` with a reason runs the verb anyway and is ledgered: use it only when the user says so.-8. **Create scratch and test geometry hidden.** [unmeasured effect]- `swx_new_document {"type":"part","visible":false}`, or `{"op":"new_part","visible":false}`- in a script, creates the document with no window. Inspection, measurement, export and- scripts all work on it. SOLIDWORKS documents that a document created hidden cannot be- shown later: to see it, save it and reopen it. A user's own script stays visible (that- is the default), because the user wants to see their part. `swx_selftest` and the quit- ladder's scratch part are hidden from 0.10.6.-9. **Reuse one document for a batch.** [unmeasured effect]- `swx_run_script {"reuseDocument": "<path of a part the bridge made>", "script": [...]}`- runs the script in that part, and its leading `new_part` becomes `clear_part`. That- step deletes every feature after the origin, and only in a part the bridge created. N- checks then cost one document. `swx_selftest` does this for every check that does not- set a material or colour or name a feature by number, and reports `documentsCreated`.+8. **Hidden documents can't be modelled.** [measured] `visible:false` creates a document+ SOLIDWORKS cannot activate, and every modelling step needs the active document, so+ `swx_run_script` refuses to model one (`document_hidden`). Everything is visible by+ default, including the self-test's parts. Use `visible:false` only for a document that+ is created to be saved or exported, never modelled. ## Diagnosing by hand (read-only)
src/comhost/com.rs+25−4@@ -5413,8 +5413,11 @@ impl ComBackend { use crate::script::Op; let script = args.get("script").cloned().unwrap_or(Value::Null); let ops = crate::script::parse(&script).map_err(HostError::msg)?;- let has_doc = self.resolve(&json!({})).is_ok();- crate::script::validate(&ops, has_doc).map_err(HostError::msg)?;+ // A HIDDEN part cannot be modelled (measured 2026-10-14): refused before+ // anything runs, never as a failure halfway through.+ if let Some(m) = crate::script::hidden_conflict(&ops) {+ return Err(HostError::coded("document_hidden", m));+ } // PIN the target. Without this every step re-resolved "the active // document", so a dialog, a rebuild that stole focus, or the user simply@@ -5430,14 +5433,32 @@ impl ComBackend { .filter(|s| !s.is_empty()) .or_else(|| args.get("targetDocument").and_then(|v| v.as_str()).filter(|s| !s.is_empty())) .map(String::from);+ let mut pinned_doc: Option<Disp> = None; if let Some(want) = &pinned { // Prove the pin resolves BEFORE running anything.- self.resolve(&Self::doc_arg(&pinned)).map_err(|e| {+ pinned_doc = Some(self.resolve(&Self::doc_arg(&pinned)).map_err(|e| { HostError::detailed( format!("cannot target document '{want}': {}", e.message), "swx_docs lists the titles that are open. Nothing was run.".to_string(), )- })?;+ })?);+ }+ // THE PINNED DOCUMENT COUNTS AS "a document" (0.10.7): measured, a+ // reuseDocument naming a part that was open but not ACTIVE failed+ // "needs a document" because only the active one was looked at.+ let has_doc = pinned_doc.is_some() || self.resolve(&json!({})).is_ok();+ crate::script::validate(&ops, has_doc).map_err(HostError::msg)?;+ if let Some(d) = &pinned_doc {+ if ops.iter().any(|o| o.needs_selection()) && d.flag("Visible", &[]) == Some(false) {+ return Err(HostError::coded(+ "document_hidden",+ format!(+ "'{}' was created invisible: {}",+ d.string("GetTitle", &[]).unwrap_or_default(),+ crate::script::HIDDEN_DOCUMENT+ ),+ ));+ } } let mut results = Vec::new();
src/comhost/mock.rs+22−2@@ -2672,6 +2672,12 @@ impl MockBackend { the titles that are actually open" )) })?;+ if self.find(&key).map(|d| !d.visible).unwrap_or(false) {+ return Err(HostError::msg(format!(+ "SOLIDWORKS could not activate '{name}' (errors=0) — it was created invisible, and a hidden \+ document cannot be activated (measured 2026-10-14)"+ )));+ } self.active = Some(key.clone()); let visibility = if args.get("visible").and_then(|v| v.as_bool()) == Some(true) { self.find(&key).map(|d| Self::make_visible_report(d.visible))@@ -3851,6 +3857,9 @@ impl MockBackend { // so a script the mock accepts is a script SOLIDWORKS will be // asked to run — that is what makes the mock worth having. let ops = crate::script::parse(&script).map_err(HostError::msg)?;+ if let Some(m) = crate::script::hidden_conflict(&ops) {+ return Err(HostError::coded("document_hidden", m));+ } crate::script::validate(&ops, !self.docs.is_empty()).map_err(HostError::msg)?; // The SAME pinning the real backend does: every step targets the // document the server named (or the one a new_part step made),@@ -3867,7 +3876,15 @@ impl MockBackend { None => json!({}), }; if pinned.is_some() {- self.resolve(&pin_arg(&pinned))?;+ let key = self.resolve(&pin_arg(&pinned))?;+ let hidden = self.find(&key).map(|d| !d.visible).unwrap_or(false);+ if hidden && ops.iter().any(|o| o.needs_selection()) {+ let title = self.find(&key).map(|d| d.title.clone()).unwrap_or_default();+ return Err(HostError::coded(+ "document_hidden",+ format!("'{title}' was created invisible: {}", crate::script::HIDDEN_DOCUMENT),+ ));+ } } // ACTIVATE THE PINNED DOCUMENT (0.10.2, review #8): selection works // only in SOLIDWORKS' active document. The user's active document@@ -3895,7 +3912,10 @@ impl MockBackend { // not made active by NewDocument, so model that here. if !fault("mock-no-activation") { if let Ok(p) = self.resolve(&pin_arg(&pinned)) {- self.active = Some(p);+ // A HIDDEN document cannot be activated (measured).+ if self.find(&p).map(|d| d.visible).unwrap_or(true) {+ self.active = Some(p);+ } } } run = fresh_run();
src/compat.rs+10−4@@ -105,8 +105,14 @@ pub const LIVE_2026_10_14: Measurement = Measurement { year: 2026, major: 34, ed /// 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" }; +/// 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" };+ const M2026: &[Measurement] = &[LIVE_2026_10_14]; 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.@@ -122,9 +128,9 @@ 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_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)", checks: "not yet run live (0.10.6)", measured: UNMEASURED },- OpFact { id: "new_assembly.invisible", what: "a new assembly created HIDDEN", checks: "not yet run live (0.10.6)", measured: UNMEASURED },- OpFact { id: "clear_part", what: "every feature after the origin deleted from the bridge's own part (DeleteSelection2)", checks: "not yet run live (0.10.6)", 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: "sketch.plane.right", what: "a sketch on Right Plane (sketch +x = model −Z, +y = model +Y; a boss goes +X)", checks: "T35", measured: M2026B },@@ -521,7 +527,7 @@ mod tests { assert_eq!( unmeasured, [- "chamfer.allOfFeature", "clear_part", "extrude.both.throughAll", "fillet.createDefinition", "new_assembly",+ "chamfer.allOfFeature", "extrude.both.throughAll", "fillet.createDefinition", "new_assembly", "new_assembly.invisible", "new_part.invisible", "rebuild", "save_as", "select.entitySelect4", "select.selectByIdAtPoint", "select.selectByRay", "set_appearance.body", "sketch.face.front.positive", "sketch.face.right.negative", "sketch.offset.front.negative",
src/launch.rs+201−1@@ -839,6 +839,11 @@ pub struct Attempt { /// can never authorise acting on a DIFFERENT process than the one it was /// made about. pub attributed_sldworks: Option<(u32, u64)>,+ /// HOW `attributed_sldworks` was established (0.10.7): `ancestry` (a parent+ /// walk back to our own chain), `newProcessInLaunchWindow` (the one+ /// sldworks.exe that was not in the baseline and was created between the+ /// attempt's start and the attach), or `reattributedAfterSignIn`.+ pub attributed_how: Option<String>, /// Did we ever see the attributed root, or anything descending from it? /// An attributed route whose chain was NEVER confirmed cannot be cleaned /// up, and that is a failure to account for what we started — not a clean@@ -918,7 +923,7 @@ impl Attempt { // terminated on either reading. "possiblyOurs": self.possibly_ours, "attributedSolidworks": self.attributed_sldworks- .map(|(p, c)| json!({"pid": p, "createdAt": c})),+ .map(|(p, c)| json!({"pid": p, "createdAt": c, "how": self.attributed_how})), "chainLastSeen": self.chain_last_seen .iter() .map(|(p, c, t)| json!([p, c, t]))@@ -1071,6 +1076,7 @@ pub fn load() -> LaunchStore { v.get("createdAt")?.as_u64()?, )) }),+ attributed_how: a.pointer("/attributedSolidworks/how").and_then(|v| v.as_str()).map(String::from), chain_last_seen: a .get("chainLastSeen") .and_then(|x| x.as_array())@@ -1432,6 +1438,7 @@ pub fn begin( possibly_ours: Vec::new(), chain_last_seen: Vec::new(), attributed_sldworks: None,+ attributed_how: None, root_confirmed: false, unattributed_log: false, foreign_chain_seen: false,@@ -1501,6 +1508,7 @@ fn unfinished_placeholder(shortcut: &str) -> Attempt { possibly_ours: Vec::new(), chain_last_seen: Vec::new(), attributed_sldworks: None,+ attributed_how: None, root_confirmed: false, unattributed_log: false, foreign_chain_seen: false,@@ -3202,6 +3210,7 @@ pub fn reattribute_after_attach(os: &dyn LaunchOs, attached: bool) -> Option<Str // ladder that builds its Identity from the COM pid — can now check // that it is talking to the same process the claim was made about. at.attributed_sldworks = Some((sw_pid, sw_created));+ at.attributed_how = Some("reattributedAfterSignIn".into()); at.last_route_before_attach = Some(route.clone()); if at.attribution.is_none() { if let (Some(pid), Some(c)) = (root, Some(created)) {@@ -3244,6 +3253,131 @@ pub fn reattribute_after_attach(os: &dyn LaunchOs, attached: bool) -> Option<Str )) } +/// WHICH SOLIDWORKS DID THIS LAUNCH START? Decided at attach (0.10.7).+///+/// MEASURED 2026-10-14 on the Connected edition: the launch recorded its root+/// (pid 53900, `startProcessPassThru`) and attached, but `attributedSolidworks`+/// stayed null — the sldworks.exe that came up (pid 70644, created 4 s after+/// the root) has ppid 40520, because the launcher chain RE-PARENTS. Only a+/// sign-in re-attribution ever set it, so a session swx_start had launched four+/// seconds earlier read `bridgeStarted:false`, and a long run could never+/// recycle it.+///+/// So, at attach: first ANCESTRY (a parent walk back to our own chain, the+/// strongest proof); failing that, THE LAUNCH WINDOW — the one sldworks.exe+/// that (a) was not in the baseline census taken before our invoke and (b) was+/// created between the attempt's start and the attach. NEVER when two could be+/// ours, never without a baseline, and never a process other than the one COM+/// is attached to (when that is known).+pub fn solidworks_attribution(+ a: &Attempt,+ census: &[ProcInfo],+ attached_pid: Option<u32>,+ ready_at: u64,+) -> Result<(u32, u64, &'static str), String> {+ let sw: Vec<&ProcInfo> = census.iter().filter(|p| p.is("SLDWORKS.exe")).collect();+ if sw.is_empty() {+ return Err("no sldworks.exe in the census".into());+ }+ let must_be_attached = |p: &ProcInfo| -> Result<(), String> {+ match attached_pid {+ Some(want) if want != p.pid => Err(format!(+ "the SOLIDWORKS this launch started (pid {}) is not the one COM is attached to (pid {want})",+ p.pid+ )),+ _ => Ok(()),+ }+ };+ // 1. ANCESTRY. The walk records its refusal reasons in the slot the sign-in+ // re-attribution reports; this probe must not overwrite them.+ let saved = reattribution_refused_because();+ let by_lineage: Vec<&ProcInfo> =+ sw.iter().copied().filter(|p| descends_from_our_chain(census, p.pid, a).is_some()).collect();+ *REATTR_REASON.lock().unwrap_or_else(|e| e.into_inner()) = saved;+ match by_lineage.as_slice() {+ [one] => {+ must_be_attached(one)?;+ let created = one.created_at.ok_or("its creation time is unknown")?;+ return Ok((one.pid, created, "ancestry"));+ }+ [] => {}+ many => {+ return Err(format!(+ "{} SOLIDWORKS processes descend from this launch's chain — never attributed when more than one could be ours",+ many.len()+ ))+ }+ }+ // 2. THE LAUNCH WINDOW.+ if !a.baseline_known {+ return Err("the census before the invoke could not be taken, so no process can be called new".into());+ }+ let new: Vec<&ProcInfo> = sw+ .iter()+ .copied()+ .filter(|p| !a.baseline_pids.contains(&p.pid))+ .filter(|p| p.created_at.map(|c| c >= a.started_at && c <= ready_at).unwrap_or(false))+ .collect();+ match new.as_slice() {+ [one] => {+ must_be_attached(one)?;+ Ok((one.pid, one.created_at.unwrap_or(0), "newProcessInLaunchWindow"))+ }+ [] => Err(format!(+ "no sldworks.exe that was not already running was created between the launch's start ({}) and the attach ({ready_at})",+ a.started_at+ )),+ many => Err(format!(+ "{} sldworks.exe processes were created in the launch window — never attributed when more than one could be ours",+ many.len()+ )),+ }+}++/// Apply [`solidworks_attribution`] to the current attempt — once, when it is+/// attached and makes no claim yet. Ledgered either way. Returns the claim.+pub fn attribute_on_attach(os: &dyn LaunchOs, attached_pid: Option<u32>) -> Option<(u32, u64)> {+ let a = attempt()?;+ if a.attributed_sldworks.is_some() {+ return a.attributed_sldworks;+ }+ if !(a.stage == Stage::Attached || a.outcome.as_deref() == Some("attached")) {+ return None;+ }+ let census = os.processes().ok()?;+ // THE WINDOW CLOSES AT THE ATTACH, not now: a SOLIDWORKS the user starts+ // tomorrow is "new" to a record from today, and must never inherit it.+ let ready_at = a.stage_at;+ match solidworks_attribution(&a, &census, attached_pid, ready_at) {+ Ok((pid, created, how)) => {+ mutate_store(|s| {+ if let Some(at) = s.attempt.as_mut() {+ if at.id == a.id && at.attributed_sldworks.is_none() {+ at.attributed_sldworks = Some((pid, created));+ at.attributed_how = Some(how.into());+ }+ }+ });+ crate::ledger::record(+ "solidworks_attributed",+ json!({"pid": pid, "createdAt": created, "how": how, "attachedPid": attached_pid,+ "launchStartedAt": a.started_at, "readyAt": ready_at,+ "launchRoot": a.attribution.as_ref().map(|r| r.pid)}),+ );+ Some((pid, created))+ }+ Err(why) => {+ static LAST: Mutex<Option<u64>> = Mutex::new(None);+ let mut last = LAST.lock().unwrap_or_else(|e| e.into_inner());+ if *last != Some(a.id) {+ *last = Some(a.id);+ crate::ledger::record("solidworks_attribution_refused", json!({"why": why, "attachedPid": attached_pid}));+ }+ None+ }+ }+}+ pub fn report(os: &dyn LaunchOs) -> Value { report_with_attach(os, false) }@@ -4005,4 +4139,70 @@ mod tests { assert_eq!(ROUTES[1], "powershell"); assert!(ROUTES.iter().all(|r| is_route(r))); }++ /// THE MEASURED CONNECTED-EDITION LAUNCH (2026-10-14): the invoke reported+ /// its root (pid 53900, created 1792014529, `startProcessPassThru`);+ /// sldworks.exe came up as pid 70644, created 1792014533, with ppid 40520 —+ /// the launcher chain re-parents, so no parent walk reaches the root.+ fn measured_launch() -> (Attempt, Vec<ProcInfo>) {+ let mut a = unfinished_placeholder("SOLIDWORKS.lnk");+ a.started_at = 1792014520;+ a.invoked_at = 1792014528;+ a.stage = Stage::Attached;+ a.stage_at = 1792014611;+ a.baseline_known = true;+ a.baseline_pids = vec![6076, 90552];+ a.attribution = Some(Attribution { pid: 53900, created_at: Some(1792014529), source: "startProcessPassThru".into() });+ a.chain = vec![p(53900, "CATSTART.exe", Some(1792014529), None)];+ a.chain_last_seen = vec![(53900, 1792014529, 1792014540)];+ let census = vec![+ p(6076, "3DEXPERIENCELauncher.exe", Some(1791900000), None),+ p(40520, "SWXDesktopHelper.exe", Some(1792014531), None),+ p(70644, "SLDWORKS.exe", Some(1792014533), Some(40520)),+ ];+ (a, census)+ }++ #[test]+ fn the_launched_solidworks_is_attributed_by_the_launch_window_when_ancestry_cannot_reach_it() {+ let (a, census) = measured_launch();+ assert_eq!(+ solidworks_attribution(&a, &census, Some(70644), a.stage_at),+ Ok((70644, 1792014533, "newProcessInLaunchWindow")),+ "the one new sldworks.exe of the window, and the one COM is attached to"+ );+ // COM is attached to some OTHER process: no claim.+ assert!(solidworks_attribution(&a, &census, Some(99999), a.stage_at).unwrap_err().contains("not the one COM is attached to"));+ // TWO new SOLIDWORKS in the window: never a guess.+ let mut two = census.clone();+ two.push(p(70700, "SLDWORKS.exe", Some(1792014560), Some(40520)));+ assert!(solidworks_attribution(&a, &two, None, a.stage_at).unwrap_err().contains("2 sldworks.exe"));+ // One that was ALREADY running (in the baseline) is never new.+ let mut pre = a.clone();+ pre.baseline_pids.push(70644);+ assert!(solidworks_attribution(&pre, &census, Some(70644), pre.stage_at).is_err());+ // Created AFTER the attach (the user's own, later): outside the window.+ let mut late = census.clone();+ late[2] = p(70644, "SLDWORKS.exe", Some(1792014700), Some(40520));+ assert!(solidworks_attribution(&a, &late, Some(70644), a.stage_at).is_err());+ // Created BEFORE the attempt started: outside the window.+ late[2] = p(70644, "SLDWORKS.exe", Some(1792014000), Some(40520));+ assert!(solidworks_attribution(&a, &late, Some(70644), a.stage_at).is_err());+ // No baseline: nothing can be called new.+ let mut blind = a.clone();+ blind.baseline_known = false;+ assert!(solidworks_attribution(&blind, &census, Some(70644), blind.stage_at).unwrap_err().contains("could not be taken"));+ }++ #[test]+ fn ancestry_is_tried_first() {+ let (a, mut census) = measured_launch();+ // sldworks.exe whose parent IS our recorded root, alive with the same identity.+ census.push(p(53900, "CATSTART.exe", Some(1792014529), None));+ census[2] = p(70644, "SLDWORKS.exe", Some(1792014533), Some(53900));+ assert_eq!(solidworks_attribution(&a, &census, Some(70644), a.stage_at), Ok((70644, 1792014533, "ancestry")));+ // Two descend from our chain: refused, even though the window would pick one.+ census.push(p(70645, "SLDWORKS.exe", Some(1792014534), Some(53900)));+ assert!(solidworks_attribution(&a, &census, None, a.stage_at).unwrap_err().contains("descend from this launch's chain"));+ } }
src/memory.rs+85−36@@ -492,6 +492,11 @@ pub fn session_facts(ctx: &crate::verbs::Ctx, os: &dyn LaunchOs) -> SessionFacts } let com_pid = state.get("processId").and_then(|v| v.as_u64()).map(|p| p as u32); let open_docs = state.get("documentCount").and_then(|v| v.as_u64());+ // A launch that attached but never named its SOLIDWORKS is attributed now+ // (0.10.7) — on the Connected edition that was every launch.+ if com_pid.is_some() {+ crate::launch::attribute_on_attach(os, com_pid);+ } let created = com_pid.and_then(|pid| os.processes().ok()?.iter().find(|p| p.pid == pid).and_then(|p| p.created_at)); SessionFacts { com_pid, open_docs, bridge_started: bridge_started(crate::launch::attributed_solidworks(), com_pid, created) } }@@ -581,45 +586,71 @@ pub fn guard(ctx: &crate::verbs::Ctx, command: &str, args: &Value) -> Result<Opt #[derive(Clone, Debug, PartialEq)] pub enum Between { Continue,+ /// Carry on, and say so: commit is HIGH, but SOLIDWORKS is within its+ /// budget — the memory is the other applications', and stopping would only+ /// throttle the run (0.10.7, measured: a run stopped before T01 at 86.9 %+ /// committed while SOLIDWORKS held 1.5 GB).+ Note(String), Recycle(String), Stop(String), } -/// Does this reading call for a decision at all? (Cheap; the session facts are-/// read only when it does.)+/// Does this reading call for a decision that needs the SESSION's facts (whose+/// it is, what is open)? Only SOLIDWORKS over its budget, or commit CRITICAL. pub fn needs_attention(p: Pressure, sw_private_mb: Option<u64>, budget_mb: u64) -> bool {- p.pressing() || sw_private_mb.map(|m| m > budget_mb).unwrap_or(false)+ p == Pressure::Critical || sw_private_mb.map(|m| m > budget_mb).unwrap_or(false) } -/// RECYCLE ONLY A SESSION THE BRIDGE STARTED, WITH NOTHING OPEN. Anything else —-/// the user's session, documents open, or a session nobody can identify —-/// STOPS the run instead.+/// THE RULE BETWEEN CHECKS (0.10.7):+/// * SOLIDWORKS OVER ITS BUDGET: recycle it if it is the bridge's own, empty+/// session; otherwise STOP — it is SOLIDWORKS that is growing.+/// * commit CRITICAL: recycle the bridge's own empty session if SOLIDWORKS+/// holds enough to matter; otherwise STOP.+/// * commit HIGH with SOLIDWORKS within budget: CARRY ON and report it — the+/// reused document keeps SOLIDWORKS flat, and the memory is elsewhere. pub fn between_checks(p: Pressure, sw_private_mb: Option<u64>, budget_mb: u64, bridge_started: Option<bool>, open_docs: Option<u64>) -> Between {- if !needs_attention(p, sw_private_mb, budget_mb) {- return Between::Continue;- }- let mut why: Vec<String> = Vec::new();- if let Some(m) = sw_private_mb.filter(|m| *m > budget_mb) {- why.push(format!("SOLIDWORKS holds {} (budget {})", gb(m), gb(budget_mb)));- }- if p.pressing() {- why.push(format!("memory pressure is {}", p.name()));+ let over = sw_private_mb.filter(|m| *m > budget_mb);+ let not_recyclable = |why: String| -> Between {+ match (bridge_started, open_docs) {+ (Some(true), Some(n)) if n > 0 => {+ Between::Stop(format!("{why}; the session is the bridge's but {n} document(s) are open, so it cannot be recycled safely"))+ }+ (Some(true), None) => Between::Stop(format!("{why}; the open-document count could not be read, so it cannot be recycled safely")),+ (Some(false), _) => Between::Stop(format!("{why}; this SOLIDWORKS was not started by the bridge, so the bridge will not end it")),+ (None, _) => Between::Stop(format!(+ "{why}; whose SOLIDWORKS session this is could not be established, so the bridge will not end it"+ )),+ (Some(true), Some(_)) => Between::Stop(why),+ }+ };+ let ours_and_empty = matches!((bridge_started, open_docs), (Some(true), Some(0)));+ if let Some(m) = over {+ let why = format!("SOLIDWORKS holds {} (budget {})", gb(m), gb(budget_mb));+ return if ours_and_empty { Between::Recycle(why) } else { not_recyclable(why) }; }- let why = why.join("; ");- let worth = sw_private_mb.map(|m| m > budget_mb || m >= RECYCLE_WORTH_MB).unwrap_or(false);- match (bridge_started, open_docs) {- (Some(true), Some(0)) if worth => Between::Recycle(why),- (Some(true), Some(0)) => Between::Stop(format!(- "{why}; SOLIDWORKS itself holds only {} — the memory is held by other applications \- (largestHolders), so recycling SOLIDWORKS would not relieve it",- sw_private_mb.map(gb).unwrap_or_else(|| "an unknown amount".into())- )),- (Some(true), Some(n)) => Between::Stop(format!(- "{why}; the session is the bridge's but {n} document(s) are open, so it cannot be recycled safely"+ match p {+ Pressure::Critical => {+ let why = "committed memory is critical".to_string();+ let worth = sw_private_mb.map(|m| m >= RECYCLE_WORTH_MB).unwrap_or(false);+ if ours_and_empty && worth {+ Between::Recycle(why)+ } else if ours_and_empty {+ Between::Stop(format!(+ "{why}; SOLIDWORKS itself holds only {} — the memory is held by other applications \+ (largestHolders), so recycling SOLIDWORKS would not relieve it",+ sw_private_mb.map(gb).unwrap_or_else(|| "an unknown amount".into())+ ))+ } else {+ not_recyclable(why)+ }+ }+ Pressure::High => Between::Note(format!(+ "committed memory is high, but SOLIDWORKS holds {} — within its {} budget — so the run carries on \+ (the memory is held by other applications; see largestHolders)",+ sw_private_mb.map(gb).unwrap_or_else(|| "an unknown amount".into()),+ gb(budget_mb) )),- (Some(true), None) => Between::Stop(format!("{why}; the open-document count could not be read, so it cannot be recycled safely")),- (Some(false), _) => Between::Stop(format!("{why}; this SOLIDWORKS was not started by the bridge, so the bridge will not end it")),- (None, _) => Between::Stop(format!("{why}; whose SOLIDWORKS session this is could not be established, so the bridge will not end it")),+ _ => Between::Continue, } } @@ -742,23 +773,41 @@ mod tests { fn a_long_run_recycles_only_its_own_empty_session_and_otherwise_stops() { let b = DEFAULT_BUDGET_MB; assert_eq!(between_checks(Pressure::Ok, Some(1_000), b, Some(false), Some(4)), Between::Continue);+ // OVER BUDGET: the bridge's own empty session is recycled; anything else stops. assert!(matches!(between_checks(Pressure::Ok, Some(b + 1), b, Some(true), Some(0)), Between::Recycle(_)));- assert!(matches!(between_checks(Pressure::High, Some(3_000), b, Some(true), Some(0)), Between::Recycle(_)));+ for (bs, d, says) in [+ (Some(true), Some(1), "1 document(s) are open"),+ (Some(true), None, "could not be read"),+ (Some(false), Some(0), "not started by the bridge"),+ (None, Some(0), "could not be established"),+ ] {+ match between_checks(Pressure::High, Some(b + 1), b, bs, d) {+ Between::Stop(why) => assert!(why.contains("SOLIDWORKS holds") && why.contains(says), "{why}"),+ other => panic!("{bs:?} {d:?}: {other:?} — only the bridge's own empty session is recycled"),+ }+ }+ // CRITICAL: recycled when it is ours, empty and SOLIDWORKS holds enough to matter... assert!(matches!(between_checks(Pressure::Critical, Some(RECYCLE_WORTH_MB), b, Some(true), Some(0)), Between::Recycle(_)));- // Pressure that SOLIDWORKS is not causing: a restart would not relieve it.+ // ...otherwise it stops. for sw in [Some(1_000), None] { match between_checks(Pressure::Critical, sw, b, Some(true), Some(0)) { Between::Stop(why) => assert!(why.contains("held by other applications"), "{why}"), other => panic!("{other:?}"), } }- for (bs, d) in [(Some(true), Some(1)), (Some(true), None), (Some(false), Some(0)), (None, Some(0)), (None, None)] {- match between_checks(Pressure::High, Some(b + 1), b, bs, d) {- Between::Stop(why) => assert!(why.contains("SOLIDWORKS holds") && why.contains("memory pressure is high"), "{why}"),- other => panic!("{bs:?} {d:?}: {other:?} — only the bridge's own empty session is recycled"),+ assert!(matches!(between_checks(Pressure::Critical, Some(1_500), b, Some(false), Some(0)), Between::Stop(_)));+ // HIGH WITH SOLIDWORKS WITHIN BUDGET: the run CARRIES ON and says so —+ // measured 2026-10-14: 86.9 % committed, Fusion 20.4 GB, SOLIDWORKS 1.5 GB,+ // and 0.10.6 stopped before T01. Whoever's session it is.+ for bs in [Some(false), Some(true), None] {+ match between_checks(Pressure::High, Some(1_536), b, bs, Some(0)) {+ Between::Note(why) => assert!(why.contains("carries on") && why.contains("1.5 GB"), "{why}"),+ other => panic!("{bs:?}: {other:?} — high pressure that is not SOLIDWORKS's must not throttle the run"), } }- assert!(matches!(between_checks(Pressure::Unknown, Some(10), b, None, None), Between::Continue), "nothing to act on");+ assert!(!needs_attention(Pressure::High, Some(1_536), b), "high alone needs no session facts");+ assert!(needs_attention(Pressure::Critical, Some(10), b) && needs_attention(Pressure::Ok, Some(b + 1), b));+ assert_eq!(between_checks(Pressure::Unknown, Some(10), b, None, None), Between::Continue); } #[test]
src/script.rs+47@@ -232,6 +232,27 @@ impl Op { } } + /// Does this op SELECT something in the document? SOLIDWORKS selects only in+ /// its ACTIVE document — a plane, a sketch, a face, an edge, a feature — and+ /// a document created invisible cannot be activated (MEASURED 2026-10-14:+ /// `new_part {visible:false}` then `sketch_rect` on Top Plane failed "could+ /// not activate 'Part1'"). So these ops cannot run in a hidden document.+ pub fn needs_selection(&self) -> bool {+ matches!(+ self,+ Op::Sketch { .. }+ | Op::Extrude(_)+ | Op::Cut(_)+ | Op::Fillet { .. }+ | Op::Chamfer { .. }+ | Op::Shell { .. }+ | Op::LinearPattern { .. }+ | Op::CircularPattern { .. }+ | Op::ClearPart+ | Op::SetAppearance { target: AppearanceTarget::FaceAt(_), .. }+ )+ }+ /// Does this op make a FEATURE in the tree (the one "last" refers to)? pub fn makes_feature(&self) -> bool { matches!(@@ -816,6 +837,32 @@ fn parse_step(step: &Value) -> Result<Op, String> { }) } +/// The message for a modelling op aimed at a HIDDEN document.+pub const HIDDEN_DOCUMENT: &str = "SOLIDWORKS can select only in the active document, and a document \+ created invisible cannot be activated (measured 2026-10-14) — so it cannot be modelled";++/// A script that makes a HIDDEN part and then models it: the step that would+/// fail mid-script, named up front (0.10.7).+pub fn hidden_conflict(ops: &[Op]) -> Option<String> {+ let mut hidden = false;+ for (i, op) in ops.iter().enumerate() {+ match op {+ Op::NewPart { visible } | Op::NewAssembly { visible } => hidden = !*visible,+ _ if hidden && op.needs_selection() => {+ return Some(format!(+ "step {} ({}) would model a part created with visible:false: {HIDDEN_DOCUMENT}. Leave \+ visible at its default (true) — to keep a battery's memory flat, reuse one document \+ (reuseDocument) instead",+ i + 1,+ op.name()+ ))+ }+ _ => {}+ }+ }+ None+}+ /// A sketch op must come before the feature that consumes it, a feature needs /// a document, and "last" needs a feature before it. Checked BEFORE the first /// COM call so a script that cannot work never half-runs on the user's live
src/selftest.rs+68−21@@ -179,12 +179,13 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { let memory_override = crate::memory::override_reason(args).ok().flatten(); let mut memory_log = MemoryLog::default(); let mut last_check: Option<&'static str> = None;- // ONE HIDDEN DOCUMENT FOR THE BATTERY (0.10.6). Measured 2026-10-14:- // each created-and-closed document cost SOLIDWORKS ~83 MB and ~125- // handles for good. Every check that can runs in ONE hidden part the- // bridge made, cleared between checks (clear_part); a check that needs- // a fresh part (a material, a colour, a feature named by its number)- // gets its own — hidden too.+ // ONE VISIBLE DOCUMENT FOR THE BATTERY (0.10.7). MEASURED 2026-10-14:+ // every NEW part costs SOLIDWORKS ~85 MB and ~125 handles until it+ // restarts, while 12 builds in ONE reused part left it flat (3260 ->+ // 3246 MB). Every check that can runs in one part the bridge made,+ // cleared between checks (clear_part); a check that needs a fresh part+ // (a material, a colour, a feature named by its number) gets its own.+ // VISIBLE: a hidden part cannot be modelled (measured). let mut shared: Option<String> = None; for check in battery() { if !only.is_empty() && !only.iter().any(|o| o == check.id) {@@ -231,7 +232,7 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { } // THE VERBS on one scratch part made by swx_new_document.- let mut new_doc = json!({"type": "part", "visible": false});+ let mut new_doc = json!({"type": "part"}); memory_log.documents_created += 1; if let Some(r) = &memory_override { new_doc["ignoreMemoryPressure"] = json!(true);@@ -284,6 +285,10 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { if let Some(prev) = &active { run.call(ctx, "re-activate the document that was active before", "swx_activate_document", json!({"name": prev})); }+ // HYGIENE (0.10.7): release every COM reference the run held. MEASURED+ // 2026-10-14: a respawn freed ~210 MB of SOLIDWORKS's memory and never+ // touched SOLIDWORKS itself.+ 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())); } @@ -295,6 +300,10 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { struct MemoryLog { /// Documents the run created (the shared part counts once). documents_created: usize,+ /// Checks that ran under HIGH commit pressure with SOLIDWORKS within budget.+ pressure_seen: Vec<Value>,+ /// The COM host respawned at the end of the run, and what it freed.+ com_respawn: Option<Value>, recycled: Vec<Value>, /// Checks run on under `ignoreMemoryPressure` where the run would have stopped. overridden: Vec<Value>,@@ -306,6 +315,8 @@ impl MemoryLog { let mut v = json!({ "recycled": self.recycled, "documentsCreated": self.documents_created,+ "memoryPressureSeen": self.pressure_seen,+ "comRespawn": self.com_respawn, "complete": self.stopped.is_none(), "memory": crate::memory::report(crate::launchos::os().as_ref()), });@@ -333,8 +344,15 @@ fn memory_between( use crate::memory::Between; let os = crate::launchos::os(); let s = crate::memory::sample(os.as_ref());- let (p, _) = s.pressure();+ let (p, pressure_why) = s.pressure(); if !crate::memory::needs_attention(p, s.sw_private_mb, s.budget_mb) {+ // HIGH, with SOLIDWORKS within its budget: carry on, and report it+ // (0.10.7 — 0.10.6 stopped a run for memory other applications held).+ if let crate::memory::Between::Note(why) = crate::memory::between_checks(p, s.sw_private_mb, s.budget_mb, None, None) {+ if log.pressure_seen.len() < 50 {+ log.pressure_seen.push(json!({"beforeCheck": next, "pressure": p.name(), "why": why, "readings": pressure_why}));+ }+ } return Ok(()); } // The run's own shared part is closed first: it is the bridge's, and a@@ -343,6 +361,10 @@ fn memory_between( let facts = crate::memory::session_facts(ctx, os.as_ref()); match crate::memory::between_checks(p, s.sw_private_mb, s.budget_mb, facts.bridge_started, facts.open_docs) { Between::Continue => Ok(()),+ Between::Note(why) => {+ log.pressure_seen.push(json!({"beforeCheck": next, "pressure": p.name(), "why": why}));+ Ok(())+ } Between::Recycle(why) => { let r = recycle(ctx, last, next, &s, &why)?; log.recycled.push(r);@@ -367,7 +389,7 @@ fn memory_between( /// Can this check run in the SHARED, cleared document? Not if it sets a /// material, colour or property (they outlive clear_part), names a feature by /// its number (numbering continues in a reused part), or checks the feature-/// list — those get a fresh, hidden part of their own.+/// list — those get a fresh part of their own. fn reusable(c: &Check) -> bool { let Some(steps) = c.script.as_array() else { return false }; if steps.first().and_then(|s| s.get("op")).and_then(|o| o.as_str()) != Some("new_part") || c.expect.features.is_some() {@@ -383,9 +405,10 @@ fn reusable(c: &Check) -> bool { }) } -/// The battery's shared part: hidden, the bridge's own.+/// The battery's shared part: VISIBLE (a hidden one cannot be modelled), the+/// bridge's own. fn make_shared_document(ctx: &Ctx, over: Option<&str>) -> Option<String> {- let mut a = json!({"type": "part", "visible": false});+ let mut a = json!({"type": "part"}); if let Some(r) = over { a["ignoreMemoryPressure"] = json!(true); a["reason"] = json!(r);@@ -406,6 +429,29 @@ fn close_shared(ctx: &Ctx, shared: &mut Option<String>) { } } +/// swx_com_respawn at the end of a write run, with SOLIDWORKS's memory read+/// either side of it.+fn respawn_com_host(ctx: &Ctx) -> Value {+ let before = crate::memory::sample(crate::launchos::os().as_ref());+ let r = crate::verbs::dispatch(+ ctx,+ "swx_com_respawn",+ &json!({"reason": "swx_selftest: release the COM references the run held (measured: about 210 MB)"}),+ );+ let after = crate::memory::sample(crate::launchos::os().as_ref());+ json!({+ "respawned": r.get("respawned").cloned().unwrap_or(json!(false)),+ "gracefulExit": r.get("gracefulExit").cloned().unwrap_or(Value::Null),+ "solidworksPrivateMbBefore": before.sw_private_mb,+ "solidworksPrivateMbAfter": after.sw_private_mb,+ "freedMb": match (before.sw_private_mb, after.sw_private_mb) {+ (Some(b), Some(a)) => json!(b as i64 - a as i64),+ _ => Value::Null,+ },+ "error": r.get("error").cloned().unwrap_or(Value::Null),+ })+}+ fn compact(v: &Value) -> Value { json!({ "success": v.get("success"),@@ -483,6 +529,15 @@ fn finish(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, #[allow(clippy::too_many_arguments)] fn finish_with(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>, extra: Value) -> Value { let recycled = extra.get("recycled").and_then(|r| r.as_array()).map(|a| a.len()).unwrap_or(0);+ let pressured = extra.get("memoryPressureSeen").and_then(|r| r.as_array()).map(|a| a.len()).unwrap_or(0);+ let lead = match pressured {+ 0 => lead,+ n => Some(format!(+ "MEMORY: committed memory was HIGH before {n} check(s), held by other applications — SOLIDWORKS stayed \+ within its budget, so the run carried on (see `memoryPressureSeen` and swx_status.memory). {}",+ lead.unwrap_or_default()+ )),+ }; let lead = match recycled { 0 => lead, n => Some(format!(@@ -789,16 +844,8 @@ fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<& let mut sargs = match reuse { // In the shared part: its new_part becomes clear_part. Some(doc) => json!({"script": c.script, "reuseDocument": doc}),- // A part of its own, HIDDEN: the self-test makes it for itself.- None => {- let mut script = c.script.clone();- if let Some(first) = script.as_array_mut().and_then(|a| a.first_mut()) {- if first.get("op").and_then(|o| o.as_str()) == Some("new_part") {- first["visible"] = json!(false);- }- }- json!({"script": script})- }+ // A part of its own (visible: a hidden part cannot be modelled).+ None => json!({"script": c.script}), }; if let Some(r) = memory_override { sargs["ignoreMemoryPressure"] = json!(true);
src/verbs.rs+14−3@@ -596,6 +596,11 @@ fn swx_status(ctx: &Ctx, _args: &Value) -> Value { // sldworks.exe on the machine (a zombie from a crash, or the user's own // session beside ours) the one a caller MEANS is the one COM answers for. crate::launch::note_attached_solidworks(com_pid);+ // WHICH SOLIDWORKS THE LAST LAUNCH STARTED (0.10.7), decided once it is+ // attached — the claim a recycle and the quit gate rely on.+ if com_pid.is_some() {+ crate::launch::attribute_on_attach(os.as_ref(), com_pid);+ } let health = crate::launch::host_app_health(os.as_ref(), open_docs, com_timed_out, com_pid); let mut solidworks = detection.to_json(); if let (Value::Object(map), Value::Object(h)) = (&mut solidworks, &health) {@@ -765,6 +770,8 @@ fn swx_start(ctx: &Ctx, args: &Value) -> Value { // clicking their Start Menu while we watched — the attempt is over and // the route that was in flight is the one that worked. crate::launch::mark_attached();+ let pid = r.get("processId").and_then(|v| v.as_u64()).map(|p| p as u32);+ crate::launch::attribute_on_attach(crate::launchos::os().as_ref(), pid); return ok_merge( r, json!({@@ -966,6 +973,12 @@ fn start_via_shortcut( if attached { crate::ledger::record_time("swx_start", started.elapsed().as_secs_f64()); crate::launch::mark_attached();+ let pid = ctx+ .call("app_state", json!({}), 20)+ .ok()+ .and_then(|r| r.get("processId").and_then(|v| v.as_u64()))+ .map(|p| p as u32);+ crate::launch::attribute_on_attach(os.as_ref(), pid); } let version = if attached { ctx.call("start", json!({"visible": arg_bool(args, "visible", true), "launch": false}), 30)@@ -2002,9 +2015,7 @@ fn prime_session( Ok(_) => return Err("a dialog is up".into()), Err(e) => return Err(format!("the window census could not be read: {e}")), }- // HIDDEN (0.10.6): a document the bridge makes purely for itself gets no- // window — each created-and-closed one cost SOLIDWORKS ~83 MB (measured).- let made = crate::verbs3::new_document(ctx, &json!({"type": "part", "visible": false}));+ let made = crate::verbs3::new_document(ctx, &json!({"type": "part"})); if made.get("success").and_then(|v| v.as_bool()) != Some(true) { return Err(format!("the scratch part could not be created: {}", made.get("error").cloned().unwrap_or(Value::Null))); }
src/verbs3.rs+35−11@@ -3924,15 +3924,14 @@ pub fn new_document(ctx: &Ctx, args: &Value) -> Value { "windowEtiquette": "background", "progress": job.finished(), "_hint": if !visible {- "A NEW HIDDEN document: it has no window, so it costs SOLIDWORKS no graphics \- context (the leading suspect for the ~83 MB each created-and-closed document \- kept, measured 2026-10-14). Every object-model verb works on it — \- swx_run_script, swx_inspect_bodies, swx_mass_properties, the exports, \- swx_save_as. SOLIDWORKS documents that a document created hidden cannot be \- shown later; to see it, save it and reopen it (swx_save_as, then swx_open). \- The hidden-creation setting was put back for the next document \- (`visibility.documentVisibleRestored`). UNMEASURED live: whether a hidden \- document avoids that cost."+ "A NEW HIDDEN document, with no window. It CANNOT BE MODELLED: SOLIDWORKS \+ selects only in the active document and a document created invisible cannot \+ be activated (measured 2026-10-14), so swx_run_script refuses modelling steps \+ on it (document_hidden). Saving and exporting work. SOLIDWORKS documents that \+ it cannot be shown later either; save it and reopen it to see it. Hiding is \+ NOT a memory fix: to keep a battery flat, reuse one VISIBLE document \+ (swx_run_script reuseDocument). The hidden-creation setting was put back \+ (`visibility.documentVisibleRestored`)." } else if titled_only { "A NEW document is open and active, with a TITLE and NO PATH — so it can only \ be named by that title, and SOLIDWORKS reuses Part1/Part2 every session. If a \@@ -4636,6 +4635,15 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value { } let _ = r; }+ if let Some(m) = crate::script::hidden_conflict(&ops) {+ return err_code(+ "document_hidden",+ m,+ "Nothing was run. A hidden document (visible:false) can be created, saved and exported, but \+ not modelled. Scripts default to a visible part; for a battery that must not grow \+ SOLIDWORKS's memory, create ONE part and pass its path as reuseDocument.",+ );+ } let makes_its_own_doc = matches!(ops.first(), Some(crate::script::Op::NewPart { .. } | crate::script::Op::NewAssembly { .. })); let has_doc = ctx@@ -4670,7 +4678,18 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value { }; let target = match resolve_document(ctx, &target_args, "swx_run_script") { Ok(t) => t,- Err(e) => return e,+ Err(mut e) => {+ // SAY WHICH ARGUMENT NAMED IT (measured 2026-10-14: a reused part+ // that could not be found read "needs a document").+ if let (Some(r), Value::Object(m)) = (&reuse, &mut e) {+ let inner = m.get("error").and_then(|v| v.as_str()).unwrap_or("").to_string();+ m.insert(+ "error".into(),+ json!(format!("the document named by reuseDocument ('{r}') is not open, or not visible: {inner}")),+ );+ }+ return e;+ } }; if reuse.is_some() && !target.owned { return err_code(@@ -4729,7 +4748,12 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value { crate::ledger::record("document_created", json!({"title": t, "via": "run_script (before a failed step)"})); } let created = e.created.clone();+ let hidden_reuse = reuse.is_some() && e.code.as_deref() == Some("document_hidden"); let mut out = sidecar_error(e);+ if let (true, Some(r), Value::Object(m)) = (hidden_reuse, &reuse, &mut out) {+ let inner = m.get("error").and_then(|v| v.as_str()).unwrap_or("").to_string();+ m.insert("error".into(), json!(format!("the document named by reuseDocument ('{r}') is hidden: {inner}")));+ } if let (Value::Object(m), Some(c)) = (&mut out, created) { m.insert("createdBeforeFailing".into(), c); }@@ -4859,7 +4883,7 @@ fn script_help(message: impl Into<String>) -> Value { map.insert( "opArgs".into(), json!({- "new_part": {"visible": "true (default) | false — created HIDDEN, with no window (ISldWorks::DocumentVisible); every object-model op works on it, and SOLIDWORKS cannot show it later (save and reopen to see it)"},+ "new_part": {"visible": "true (default) | false — created HIDDEN (ISldWorks::DocumentVisible). A hidden part CANNOT BE MODELLED (SOLIDWORKS selects only in the active document, and a hidden one cannot be activated — measured): a script that models it is refused document_hidden. Use reuseDocument, not hiding, to keep memory flat"}, "new_assembly": {"visible": "as new_part"}, "clear_part": {"(none)": "deletes EVERY feature after the origin of a part the BRIDGE created — never anyone else's; N checks then cost one document"}, "sketch": {"plane": "Front Plane|Top Plane|Right Plane|<any plane name> | {offsetFrom, offsetMm}", "face": "{atPointMm:[x,y,z]} instead of plane", "entities": "[{rect:{widthMm, heightMm, xMm, yMm, anchor:corner|center, centerMm:[x,y]}}, {circle:{radiusMm|diameterMm, xMm, yMm | centerMm}}] — ALL in one sketch: concentric circles make a ring, a rect with circles inside makes a plate with holes"},
tests/memory.rs+90@@ -341,3 +341,93 @@ fn a_recycle_that_cannot_quit_stops_the_run_and_says_where() { ); check!(b, "...and nothing was terminated", m.nothing_terminated(), "terminated.jsonl exists"); }++/// ATTRIBUTION ON THE CONNECTED EDITION (0.10.7), with the measured numbers:+/// the launch's root was pid 53900 (created 1792014529, startProcessPassThru),+/// sldworks.exe came up as pid 70644 (created 1792014533) with ppid 40520 — the+/// chain re-parents, so no parent walk reaches the root, and 0.10.6 left+/// `attributedSolidworks` null and `bridgeStarted:false` for a session it had+/// launched four seconds earlier. The ONE new sldworks.exe of the launch window+/// is now the bridge's.+#[test]+fn a_launch_whose_chain_reparents_still_owns_the_solidworks_it_started() {+ let m = Machine::new("mem-attrib", incident(4300), 10854);+ std::fs::write(m.dir.join("sldworks-created"), "1792014533").unwrap();+ std::fs::write(m.dir.join("sldworks-parent"), "40520").unwrap();+ let scratch = TempDir::new("mem-attrib-scratch");+ std::fs::write(scratch.join("mock-solidworks-session"), "70644").unwrap();+ std::fs::write(+ scratch.join("launch-state.json"),+ json!({+ "attempt": {+ "id": 7, "startedAt": 1792014520u64, "shortcut": "SOLIDWORKS.lnk", "route": "startProcess",+ "invokedAt": 1792014528u64, "stage": "attached", "stageAt": 1792014611u64, "finished": true,+ "outcome": "attached", "runBeginSeen": true, "baselineKnown": true, "baselinePids": [6076, 90552],+ "attribution": {"pid": 53900, "createdAt": 1792014529u64, "source": "startProcessPassThru"},+ },+ "preferredRoute": "startProcess", "consecutiveFailures": 0,+ })+ .to_string(),+ )+ .unwrap();+ let b = Bridge::start_in(scratch, &m.env());+ b.call("swx_start", json!({}));+ let st = b.call("swx_status", json!({}));+ let claim = st.pointer("/launch/attributedSolidworks").cloned().unwrap_or(Value::Null);+ check!(+ b,+ "swx_status: the launch now names its SOLIDWORKS — the one new sldworks.exe of the launch window",+ claim == json!({"pid": 70644, "createdAt": 1792014533u64, "how": "newProcessInLaunchWindow"}),+ s(&st["launch"])+ );+ check!(+ b,+ "...ledgered with how it was decided",+ ledger(&b).iter().any(|e| e["action"] == "solidworks_attributed" && e["detail"]["how"] == "newProcessInLaunchWindow"),+ s(&json!(ledger(&b)))+ );+ let r = b.call("swx_new_document", json!({"type": "part"}));+ check!(+ b,+ "so the memory guard knows the session is the bridge's (recyclable when empty)",+ r["errorCode"] == "low_commit_memory" && r["solidworksSession"]["bridgeStarted"] == true+ && r["remedy"].as_str().unwrap_or("").contains("swx_stop"),+ s(&r)+ );+}++/// THE WINDOW CLOSES AT THE ATTACH: a SOLIDWORKS created AFTER the launch+/// attached (the user's own, started later) is never the launch's, however new+/// it is to the record.+#[test]+fn a_solidworks_started_after_the_attach_is_never_claimed() {+ let m = Machine::new("mem-attrib-late", incident(40000), 900);+ std::fs::write(m.dir.join("sldworks-created"), "1792014700").unwrap();+ std::fs::write(m.dir.join("sldworks-parent"), "40520").unwrap();+ let scratch = TempDir::new("mem-attrib-late-scratch");+ std::fs::write(scratch.join("mock-solidworks-session"), "70644").unwrap();+ std::fs::write(+ scratch.join("launch-state.json"),+ json!({+ "attempt": {+ "id": 8, "startedAt": 1792014520u64, "shortcut": "SOLIDWORKS.lnk", "route": "startProcess",+ "invokedAt": 1792014528u64, "stage": "attached", "stageAt": 1792014611u64, "finished": true,+ "outcome": "attached", "runBeginSeen": true, "baselineKnown": true, "baselinePids": [6076],+ "attribution": {"pid": 53900, "createdAt": 1792014529u64, "source": "startProcessPassThru"},+ },+ "preferredRoute": "startProcess", "consecutiveFailures": 0,+ })+ .to_string(),+ )+ .unwrap();+ let b = Bridge::start_in(scratch, &m.env());+ b.call("swx_start", json!({}));+ let st = b.call("swx_status", json!({}));+ check!(+ b,+ "swx_status: a sldworks.exe created after the attach is NOT claimed, and the refusal is ledgered",+ st.pointer("/launch/attributedSolidworks").map(|v| v.is_null()).unwrap_or(true)+ && ledger(&b).iter().any(|e| e["action"] == "solidworks_attribution_refused"),+ s(&st["launch"])+ );+}
tests/quit.rs+8@@ -618,6 +618,14 @@ fn a_never_had_a_document_session_is_primed_and_then_quits() { s(&r) ); check!(b, "swx_stop: the prime is in the ledger", ledger_has(&b, "quit_prime"), String::new());+ // 0.10.7: VISIBLE — a hidden document is not what was measured to prime a+ // session, and 0.10.6's hidden prime was reverted with every other default.+ check!(+ b,+ "swx_stop: the scratch part that primed it was created VISIBLE (no DocumentVisible call)",+ !b.scratch.join("document-visible.jsonl").exists(),+ String::new()+ ); } /// Priming needs the user's authorization: under the write opt-in alone, a
tests/visibility.rs+226−163@@ -6,13 +6,13 @@ //! handles 2845 -> 4539, +~125 per part; GetDocumentCount back at 0 every time. //! So every created-and-closed document cost ~83 MB and ~125 handles for good. //!-//! This release: documents the bridge makes for itself are created HIDDEN-//! (ISldWorks::DocumentVisible bracketed around NewDocument — a SESSION-//! setting, put back on every path); a battery reuses ONE part (clear_part);-//! and `swx_com_respawn` settles whether the bridge itself pins anything.-//!-//! All in the SIMULATOR: whether hiding or reuse actually stops the growth is-//! UNMEASURED until it runs live.+//! 0.10.6 tried HIDDEN documents (ISldWorks::DocumentVisible around+//! NewDocument) and ONE reused part (clear_part). MEASURED LIVE (0.10.7's+//! brief): a hidden document cannot be modelled — SOLIDWORKS selects only in+//! the active document and a hidden one cannot be activated — while twelve+//! builds in one reused, VISIBLE part left SOLIDWORKS flat. So 0.10.7 makes+//! everything visible by default, refuses to model a hidden document up front+//! (`document_hidden`), and runs batteries in one reused part. mod common; @@ -55,67 +55,6 @@ fn box_script(first: Value) -> Value { {"op": "extrude", "depthMm": 10}]) } -#[test]-fn a_hidden_document_is_created_with_the_session_setting_bracketed_and_put_back() {- let (b, _m) = boot("vis-hidden", &[]);- b.call("swx_start", json!({}));- let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));- check!(- b,- "swx_new_document visible:false: created hidden, DocumentVisible put back, a scratch file of its own",- r["success"] == true && r["visibility"]["requested"] == false && r["visibility"]["documentVisibleRestored"] == true- && r["visibility"]["visibleReadBack"] == false && r["visibility"]["scratchSaved"] == true- && r["documentIdentity"]["path"].as_str().unwrap_or("").ends_with(".SLDPRT"),- s(&r)- );- let calls = visible_calls(&b);- check!(- b,- "DocumentVisible(false), then DocumentVisible(true) — in that order, for parts",- calls.len() == 2 && calls[0]["visible"] == false && calls[1]["visible"] == true && calls[0]["type"] == "part",- s(&json!(calls))- );- check!(- b,- "the toggle and the hidden creation are ledgered",- ledger(&b).iter().any(|e| e["action"] == "document_visible_toggled")- && ledger(&b).iter().any(|e| e["action"] == "document_created" && e["detail"]["visible"] == false),- s(&json!(ledger(&b)))- );- // EVERY OBJECT-MODEL VERB WORKS ON IT.- let path = r["documentIdentity"]["path"].as_str().unwrap_or("").to_string();- let built = b.call("swx_run_script", json!({"path": path, "script": [- {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},- {"op": "extrude", "depthMm": 10}]}));- let ib = b.call("swx_inspect_bodies", json!({"path": path}));- let mp = b.call("swx_mass_properties", json!({"path": path}));- let out = b.scratch.join("hidden.step");- let ex = b.call("swx_export_step", json!({"path": path, "outputPath": out.to_string_lossy()}));- check!(- b,- "a script, swx_inspect_bodies, swx_mass_properties and swx_export_step all work on the hidden part",- built["success"] == true && ib["bodyCount"] == 1 && ib["bodies"][0]["volumeMm3"].as_f64().map(|v| (v - 4000.0).abs() < 1e-6) == Some(true)- && mp["success"] == true && ex["success"] == true,- format!("{}\n{}\n{}\n{}", s(&built), s(&ib), s(&mp), s(&ex))- );- let shown = b.call("swx_show_part", json!({"path": path}));- check!(- b,- "swx_show_part on it: attempted, read back, and SAID to stay hidden (as SOLIDWORKS documents it)",- shown["visibility"]["wasVisible"] == false && shown["visibility"]["madeVisible"] == false- && shown["visibility"]["note"].as_str().unwrap_or("").contains("cannot be made visible"),- s(&shown)- );- // THE DEFAULT IS UNCHANGED: a user's document is visible, and nothing toggles.- let v = b.call("swx_new_document", json!({"type": "part"}));- check!(- b,- "swx_new_document with no visible: a visible document, and no DocumentVisible call at all",- v["success"] == true && v.get("visibility").is_none() && visible_calls(&b).len() == 2,- s(&v)- );-}- #[test] fn a_hidden_creation_that_fails_still_puts_the_setting_back() { let (b, _m) = boot("vis-fail", &[]);@@ -168,70 +107,6 @@ fn a_setting_the_host_could_not_put_back_is_retried_and_otherwise_shouted() { ); } -#[test]-fn a_users_script_stays_visible_by_default_and_new_part_can_hide() {- let (b, _m) = boot("vis-script", &[]);- b.call("swx_start", json!({}));- let r = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part"}))}));- check!(- b,- "new_part with no visible: visible, and no DocumentVisible call (Michael wants to see his part)",- r["success"] == true && r["steps"][0]["result"].get("visibility").is_none() && visible_calls(&b).is_empty(),- s(&r)- );- let r = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part", "visible": false}))}));- check!(- b,- "new_part visible:false: hidden, the setting put back, and its variant is UNMEASURED live",- r["success"] == true && r["steps"][0]["result"]["visibility"]["documentVisibleRestored"] == true- && r["steps"][0]["result"]["proven"]["variants"] == json!(["new_part.invisible"])- && r["steps"][0]["result"]["proven"]["live"] == false,- s(&r)- );- let bad = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part", "visible": "no"}))}));- check!(b, "visible must be a boolean", bad["errorCode"] == "bad_script", s(&bad));-}--#[test]-fn a_battery_reuses_one_part_and_clear_part_never_touches_anyone_elses() {- let (b, _m) = boot("vis-reuse", &[("SWX_ALLOW_USER_WRITES", "1")]);- b.call("swx_start", json!({}));- let made = b.call("swx_new_document", json!({"type": "part", "visible": false}));- let p = made["documentIdentity"]["path"].as_str().unwrap_or("").to_string();- let first = b.call("swx_run_script", json!({"reuseDocument": p, "script": box_script(json!({"op": "new_part"}))}));- let docs = doc_count(&b);- let second = b.call("swx_run_script", json!({"reuseDocument": p, "script": [- {"op": "new_part"},- {"op": "sketch_circle", "plane": "Top Plane", "radiusMm": 5},- {"op": "extrude", "depthMm": 10}]}));- let ib = b.call("swx_inspect_bodies", json!({"path": p}));- check!(- b,- "reuseDocument: the leading new_part became clear_part; the box is gone, only the cylinder is left",- first["success"] == true && second["success"] == true && second["steps"][0]["op"] == "clear_part"- && second["steps"][0]["result"]["featuresDeleted"] == 1- && ib["bodyCount"] == 1- && ib["bodies"][0]["volumeMm3"].as_f64().map(|v| (v - std::f64::consts::PI * 250.0).abs() < 1e-3) == Some(true),- format!("{}\n{}", s(&second), s(&ib))- );- check!(b, "...and no document was created by the reused runs", doc_count(&b) == docs, format!("{docs} -> {}", doc_count(&b)));- // A USER'S part.- let user = "C:\\mock\\plate.sldprt";- b.call("swx_open", json!({"path": user}));- let r = b.call("swx_run_script", json!({"path": user, "script": [{"op": "clear_part"}],- "allowUserWrites": true, "confirm": true, "reason": "the user asked to clear it"}));- check!(- b,- "clear_part on a part the bridge did not create: refused by the server, write grant or not",- r["errorCode"] == "clear_part_not_owned" && r["error"].as_str().unwrap_or("").contains("is not a part this bridge created"),- s(&r)- );- let r = b.call("swx_run_script", json!({"reuseDocument": user, "script": box_script(json!({"op": "new_part"}))}));- check!(b, "reuseDocument naming the user's part: refused", r["errorCode"] == "reuse_not_owned", s(&r));- let r = b.call("swx_run_script", json!({"reuseDocument": p, "script": [{"op": "new_part"}, {"op": "new_part"}]}));- check!(b, "with reuseDocument, new_part only as the first step", r["errorCode"] == "bad_script", s(&r));-}- #[test] fn com_respawn_replaces_only_the_bridges_host_and_never_calls_solidworks() { let (b, _m) = boot("vis-respawn", &[("SWX_MOCK_SLOW_MS", "2500")]);@@ -277,37 +152,6 @@ fn com_respawn_replaces_only_the_bridges_host_and_never_calls_solidworks() { ); } -#[test]-fn the_selftest_builds_hidden_and_reuses_one_document() {- let (b, _m) = boot("vis-selftest", &[]);- b.call("swx_start", json!({}));- let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03", "T15", "T17"]}));- let rows = r["results"].as_array().cloned().unwrap_or_default();- let row = |id: &str| rows.iter().find(|x| x["name"].as_str().unwrap_or("").starts_with(id)).cloned().unwrap_or(Value::Null);- check!(- b,- "every check passes; T01 and T03 ran in the shared part, T15 (a colour) and T17 (a feature by name) in their own",- r["summary"]["failed"] == 0 && row("T01")["pass"] == true && row("T03")["pass"] == true- && row("T01")["reusedDocument"] == true && row("T03")["reusedDocument"] == true- && row("T15")["reusedDocument"] == false && row("T17")["reusedDocument"] == false,- s(&r)- );- check!(- b,- "documentsCreated: one shared part, two of their own, one scratch part for the verbs — 4, not 5",- r["documentsCreated"] == 4,- s(&r["documentsCreated"])- );- let calls = visible_calls(&b);- let hidden = calls.iter().filter(|c| c["visible"] == false).count();- check!(- b,- "every one of them was created HIDDEN, and the setting was put back after each",- hidden == 4 && calls.len() == 8 && calls.chunks(2).all(|p| p[0]["visible"] == false && p[1]["visible"] == true),- s(&json!(calls))- );-}- /// THE MEASURED GROWTH, SIMULATED: every created document leaves SOLIDWORKS /// 83 MB heavier (`growthPerDocumentMB`). A reused check creates nothing, so /// it costs nothing; and when the budget is reached mid-battery, the run@@ -372,3 +216,222 @@ fn reuse_keeps_solidworks_flat_and_the_shared_part_is_closed_before_a_recycle() ); check!(b, "nothing was terminated", !machine.join("terminated.jsonl").exists(), "terminated.jsonl"); }++#[test]+fn a_hidden_document_is_bracketed_but_cannot_be_modelled() {+ let (b, _m) = boot("vis-hidden", &[]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));+ check!(+ b,+ "swx_new_document visible:false: created hidden, DocumentVisible put back, a scratch file of its own",+ r["success"] == true && r["visibility"]["documentVisibleRestored"] == true+ && r["visibility"]["visibleReadBack"] == false && r["visibility"]["scratchSaved"] == true+ && r["_hint"].as_str().unwrap_or("").contains("CANNOT BE MODELLED"),+ s(&r)+ );+ let calls = visible_calls(&b);+ check!(+ b,+ "DocumentVisible(false), then DocumentVisible(true), and ledgered",+ calls.len() == 2 && calls[0]["visible"] == false && calls[1]["visible"] == true+ && ledger(&b).iter().any(|e| e["action"] == "document_visible_toggled"),+ s(&json!(calls))+ );+ let path = r["documentIdentity"]["path"].as_str().unwrap_or("").to_string();+ let built = b.call("swx_run_script", json!({"path": path, "script": [+ {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},+ {"op": "extrude", "depthMm": 10}]}));+ let ib = b.call("swx_inspect_bodies", json!({"path": path}));+ check!(+ b,+ "modelling it is refused UP FRONT (document_hidden), naming why — nothing was built",+ built["errorCode"] == "document_hidden"+ && built["error"].as_str().unwrap_or("").contains("cannot be activated")+ && ib["bodyCount"] == 0,+ format!("{}\n{}", s(&built), s(&ib))+ );+ let prop = b.call("swx_run_script", json!({"path": path, "script": [{"op": "set_prop", "name": "Kind", "value": "hidden"}]}));+ let out = b.scratch.join("hidden.step");+ let ex = b.call("swx_export_step", json!({"path": path, "outputPath": out.to_string_lossy()}));+ check!(b, "what selects nothing still works on it: a property, an export", prop["success"] == true && ex["success"] == true, format!("{}\n{}", s(&prop), s(&ex)));+ let shown = b.call("swx_show_part", json!({"path": path}));+ check!(+ b,+ "swx_show_part on it: attempted, read back, and SAID to stay hidden",+ shown["visibility"]["madeVisible"] == false && shown["visibility"]["note"].as_str().unwrap_or("").contains("cannot be made visible"),+ s(&shown)+ );+ let v = b.call("swx_new_document", json!({"type": "part"}));+ check!(+ b,+ "swx_new_document with no visible: a visible document, and no DocumentVisible call at all",+ v["success"] == true && v.get("visibility").is_none() && visible_calls(&b).len() == 2,+ s(&v)+ );+}++#[test]+fn a_script_is_visible_by_default_and_a_hidden_part_is_never_modelled() {+ let (b, _m) = boot("vis-script", &[]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part"}))}));+ check!(+ b,+ "new_part with no visible: visible, and no DocumentVisible call (Michael wants to see his part)",+ r["success"] == true && r["steps"][0]["result"].get("visibility").is_none() && visible_calls(&b).is_empty(),+ s(&r)+ );+ let docs = doc_count(&b);+ let r = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part", "visible": false}))}));+ check!(+ b,+ "new_part visible:false then a sketch: refused document_hidden BEFORE anything ran — no part made, no setting touched",+ r["errorCode"] == "document_hidden" && r["error"].as_str().unwrap_or("").contains("step 2 (sketch_rect)")+ && r["_hint"].as_str().unwrap_or("").starts_with("Nothing was run")+ && doc_count(&b) == docs && visible_calls(&b).is_empty(),+ s(&r)+ );+ let r = b.call("swx_run_script", json!({"script": [{"op": "new_part", "visible": false}, {"op": "set_prop", "name": "Kind", "value": "hidden"}]}));+ check!(+ b,+ "a hidden part that is only given a property: allowed, and its variant is unmeasured live",+ r["success"] == true && r["steps"][0]["result"]["proven"]["variants"] == json!(["new_part.invisible"])+ && r["steps"][0]["result"]["proven"]["live"] == false,+ s(&r)+ );+ let bad = b.call("swx_run_script", json!({"script": box_script(json!({"op": "new_part", "visible": "no"}))}));+ check!(b, "visible must be a boolean", bad["errorCode"] == "bad_script", s(&bad));+}++#[test]+fn a_battery_reuses_one_visible_part_and_clear_part_never_touches_anyone_elses() {+ let (b, _m) = boot("vis-reuse", &[("SWX_ALLOW_USER_WRITES", "1")]);+ // The simulated release is the one clear_part was measured on.+ std::fs::write(b.scratch.join("mock-revision"), "34.3.0").unwrap();+ b.call("swx_start", json!({}));+ let made = b.call("swx_new_document", json!({"type": "part"}));+ let p = made["documentIdentity"]["path"].as_str().unwrap_or("").to_string();+ let first = b.call("swx_run_script", json!({"reuseDocument": p, "script": box_script(json!({"op": "new_part"}))}));+ let docs = doc_count(&b);+ let second = b.call("swx_run_script", json!({"reuseDocument": p, "script": [+ {"op": "new_part"},+ {"op": "sketch_circle", "plane": "Top Plane", "radiusMm": 5},+ {"op": "extrude", "depthMm": 10}]}));+ let ib = b.call("swx_inspect_bodies", json!({"path": p}));+ check!(+ b,+ "reuseDocument: the leading new_part became clear_part; the box is gone, only the cylinder is left",+ first["success"] == true && second["success"] == true && second["steps"][0]["op"] == "clear_part"+ && second["steps"][0]["result"]["featuresDeleted"] == 1+ && second["steps"][0]["result"]["proven"]["live"] == true+ && ib["bodyCount"] == 1+ && ib["bodies"][0]["volumeMm3"].as_f64().map(|v| (v - std::f64::consts::PI * 250.0).abs() < 1e-3) == Some(true),+ format!("{}\n{}", s(&second), s(&ib))+ );+ check!(b, "...and no document was created by the reused runs", doc_count(&b) == docs, format!("{docs} -> {}", doc_count(&b)));+ // A USER'S part.+ let user = "C:\\mock\\plate.sldprt";+ b.call("swx_open", json!({"path": user}));+ let r = b.call("swx_run_script", json!({"path": user, "script": [{"op": "clear_part"}],+ "allowUserWrites": true, "confirm": true, "reason": "the user asked to clear it"}));+ check!(+ b,+ "clear_part on a part the bridge did not create: refused by the server, write grant or not",+ r["errorCode"] == "clear_part_not_owned" && r["error"].as_str().unwrap_or("").contains("is not a part this bridge created"),+ s(&r)+ );+ let r = b.call("swx_run_script", json!({"reuseDocument": user, "script": box_script(json!({"op": "new_part"}))}));+ check!(b, "reuseDocument naming the user's part: refused", r["errorCode"] == "reuse_not_owned", s(&r));+ let r = b.call("swx_run_script", json!({"reuseDocument": p, "script": [{"op": "new_part"}, {"op": "new_part"}]}));+ check!(b, "with reuseDocument, new_part only as the first step", r["errorCode"] == "bad_script", s(&r));+ // MEASURED (D): a reuseDocument that did not resolve read "needs a document".+ let gone = b.call("swx_run_script", json!({"reuseDocument": "C:\\nowhere\\gone.SLDPRT", "script": box_script(json!({"op": "new_part"}))}));+ check!(+ b,+ "a reuseDocument that is not open: the error names reuseDocument",+ gone["success"] == false && gone["error"].as_str().unwrap_or("").contains("the document named by reuseDocument"),+ s(&gone)+ );+ let hidden = b.call("swx_new_document", json!({"type": "part", "visible": false}));+ let hp = hidden["documentIdentity"]["path"].as_str().unwrap_or("").to_string();+ let r = b.call("swx_run_script", json!({"reuseDocument": hp, "script": box_script(json!({"op": "new_part"}))}));+ check!(+ b,+ "a reuseDocument that is HIDDEN: document_hidden, and the error names reuseDocument",+ r["errorCode"] == "document_hidden" && r["error"].as_str().unwrap_or("").contains("the document named by reuseDocument")+ && r["error"].as_str().unwrap_or("").contains("is hidden"),+ s(&r)+ );+}++#[test]+fn the_selftest_reuses_one_visible_document_and_releases_the_com_host_at_the_end() {+ let (b, _m) = boot("vis-selftest", &[]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03", "T15", "T17"]}));+ let rows = r["results"].as_array().cloned().unwrap_or_default();+ let row = |id: &str| rows.iter().find(|x| x["name"].as_str().unwrap_or("").starts_with(id)).cloned().unwrap_or(Value::Null);+ check!(+ b,+ "every check passes; T01 and T03 in the shared part, T15 (a colour) and T17 (a feature by name) in their own",+ r["summary"]["failed"] == 0 && row("T01")["pass"] == true && row("T03")["pass"] == true+ && row("T01")["reusedDocument"] == true && row("T03")["reusedDocument"] == true+ && row("T15")["reusedDocument"] == false && row("T17")["reusedDocument"] == false,+ s(&r)+ );+ check!(+ b,+ "documentsCreated: one shared part, two of their own, one scratch part for the verbs — 4, not 5",+ r["documentsCreated"] == 4,+ s(&r["documentsCreated"])+ );+ check!(+ b,+ "everything VISIBLE: not one DocumentVisible call (a hidden part cannot be modelled)",+ visible_calls(&b).is_empty(),+ s(&json!(visible_calls(&b)))+ );+ check!(+ b,+ "the run ends by respawning the COM host, and reports it",+ r["comRespawn"]["respawned"] == true && r["comRespawn"]["gracefulExit"] == true+ && ledger(&b).iter().any(|e| e["action"] == "com_host_respawn"),+ s(&r["comRespawn"])+ );+}++/// MEASURED 2026-10-14: at 86.9 % committed (9.9 GB available; Fusion 20.4 GB,+/// Chrome 9.7 GB, SOLIDWORKS only 1.5 GB), 0.10.6's self-test STOPPED before+/// T01. High pressure that SOLIDWORKS is not causing must not throttle the run.+#[test]+fn high_pressure_held_by_other_applications_does_not_stop_the_selftest() {+ let machine = TempDir::new("vis-high");+ std::fs::write(machine.join("windows.json"), "[]").unwrap();+ std::fs::write(+ machine.join("sldworks.json"),+ json!({"responding": true, "hwnd": 6622700, "privateMB": 1536, "handles": 4000}).to_string(),+ )+ .unwrap();+ std::fs::write(+ machine.join("memory.json"),+ json!({"commitLimitMB": 78438, "commitAvailMB": 10138, "physTotalMB": 32153, "physAvailMB": 2000,+ "processes": [{"name": "Fusion360.exe", "pid": 41000, "privateMB": 20890},+ {"name": "chrome.exe", "pid": 42000, "privateMB": 9933}]}).to_string(),+ )+ .unwrap();+ let b = Bridge::start_with(&[("SWX_LAUNCH_FAKE_DIR", Box::leak(machine.path().into_boxed_str()))]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03"]}));+ check!(+ b,+ "the run COMPLETES at high pressure: no stop, no recycle, and the pressure is reported per check",+ r["complete"] == true && r.get("stoppedFor").is_none() && r["recycled"] == json!([])+ && r["memoryPressureSeen"].as_array().map(|a| a.len() >= 2).unwrap_or(false)+ && r["memoryPressureSeen"][0]["why"].as_str().unwrap_or("").contains("carries on")+ && r["summary"]["failed"] == 0+ && r["_hint"].as_str().unwrap_or("").contains("held by other applications"),+ s(&r)+ );+ check!(b, "nothing was quit or terminated", !machine.join("quit.jsonl").exists() && !machine.join("terminated.jsonl").exists(), "quit / terminated");+}
uninstall.sh