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.6 source
27 files changed
+1461−73
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+33−3bridge.json+2−1docs/COMPATIBILITY.md+19−2install.shpackage.json+1−1references/modelling-and-export.md+14skills/solidworks-memory/SKILL.md+45−4src/comhost/com.rs+217−19src/comhost/host.rs+7src/comhost/mock.rs+206−7src/comhost/mock_model.rs+10src/comhost/mod.rs+44src/compat.rs+12−3src/launchos.rs+8−1src/memory.rs+91src/script.rs+33−8src/selftest.rs+101−7src/verbs.rs+65−1src/verbs3.rs+163−11tests/e2e.rs+11tests/readiness.rs+1−1tests/visibility.rs+374tests/wiring.rs+1−1uninstall.shBRIDGE_VERSION+1−1@@ -1 +1 @@-0.10.5+0.10.6
Cargo.lock+1−1@@ -122,7 +122,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.10.5"+version = "0.10.6" dependencies = [ "miniz_oxide", "serde",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.5"+version = "0.10.6" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+33−3@@ -9,7 +9,7 @@ One native binary. No Python, no PowerShell, no interpreter to provision: ab spawns `swx-bridge.exe` directly (`spawn.kind: "exe"`) and it is serving in about ten milliseconds. -## What it does — 70 verbs+## What it does — 71 verbs | Area | Verbs | |---|---|@@ -23,7 +23,7 @@ about ten milliseconds. | Read the design back | `swx_design_info`, `swx_inspect_bodies` (geometry in mm), `swx_get_parameters`, `swx_set_parameter`, `swx_mass_properties`, `swx_bom`, `swx_interference`, `swx_rebuild_check`, `swx_document_info` | | Metadata | `swx_props_get`, `swx_props_set` (custom properties, file- or configuration-scope) | | Sign-in | `swx_sign_in` — signs in to 3DEXPERIENCE for a launch this bridge started, from the user's own local vault; the password never reaches a reply, a log or the container, and a rejected password is never retried. |-| App & documents | `swx_get_app_state`, `swx_state`, `swx_status`, `swx_readiness`, `swx_progress`, `swx_start`, `swx_stop`, `swx_kill`, `swx_activate_document`, `swx_close_all_documents`, `swx_reset_workspace`, `swx_show_part/_assembly/_drawing` |+| App & documents | `swx_get_app_state`, `swx_state`, `swx_status`, `swx_readiness`, `swx_progress`, `swx_start`, `swx_stop`, `swx_kill`, `swx_com_respawn` (restarts only the bridge's own COM host, to release every reference it holds), `swx_activate_document`, `swx_close_all_documents`, `swx_reset_workspace`, `swx_show_part/_assembly/_drawing` | | Windows & etiquette | `swx_window_info`, `swx_dismiss_blocking_dialogs`, `swx_close_window`, `swx_check_recovery`, `swx_screenshot`, `swx_set_driving_badge`, `swx_action_ledger`, `swx_verb_times` | | Preferences & cloud | `swx_get_preferences`, `swx_set_preference`, `swx_cloud_status` | | Bridge | `swx_describe`, `swx_com_eval`, `swx_uninstall`, `swx_selftest` (one JSON saying what works on THIS SOLIDWORKS install — see [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md)) |@@ -498,7 +498,37 @@ not. ## Roadmap -- v0.10.5 (this): **memory, after a live incident.** On 2026-10-14 the+- v0.10.6 (this): **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).+ `GetDocumentCount` was back at 0 every time. Every created-and-closed document+ costs about 83 MB for good. Drew: "Throttling is a painful solution."+ - **Hidden documents.** `visible:false` on `swx_new_document` and on the+ script's `new_part` / `new_assembly`. It wraps NewDocument in+ `ISldWorks::DocumentVisible(false … true)`. That is a session setting, so it+ is put back on every path, including when the creation fails. It is retried,+ the server tries again if needed, and both are ledgered. The self-test and+ the quit ladder's scratch part are hidden; user scripts stay visible by+ default. A hidden document gets its scratch file through the document object+ itself, since it may not be the active document.+ - **One document for a battery.** A new `clear_part` op deletes every feature+ after the origin, using `DeleteSelection2` with children and absorbed+ features, and only in a part the bridge created. `swx_run_script+ {"reuseDocument": …}` turns a script's leading `new_part` into `clear_part`.+ The self-test runs every check it can in one hidden part, and reports+ `documentsCreated` and `reusedDocument` per row.+ - **`swx_com_respawn {reason}`.** It restarts only the bridge's own COM host,+ which releases every COM reference the bridge holds. It never calls+ SOLIDWORKS, refuses while a verb is in flight, and is ledgered. It settles+ whether the bridge pins anything.+ - The 0.10.5 recycle stays as the safety net.+ - `swx_show_*` now activates with `swDontRebuildActiveDoc`. It passed 0, which+ is the prompting `swUserDecision`.+ - Whether hiding or reuse stops the growth is unmeasured.++ 623 tests.+- v0.10.5: **memory, after a live incident.** On 2026-10-14 the SOLIDWORKS Resource Monitor warned that "available committed memory is critically low": 72.3 of 76.6 GB was committed. The bridge-started SOLIDWORKS held 10.6 GB with no documents open, after about a hundred scratch parts had
bridge.json+2−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.10.5",+ "version": "0.10.6", "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",@@ -32,6 +32,7 @@ "swx_close_window", "swx_cloud_status", "swx_com_eval",+ "swx_com_respawn", "swx_describe", "swx_design_info", "swx_dismiss_blocking_dialogs",
docs/COMPATIBILITY.md+19−2@@ -12,9 +12,10 @@ only ever **run** against **3DEXPERIENCE SOLIDWORKS 2026** (Makers, revision ## The answer - **[audited] Supported from SOLIDWORKS 2021.** Every COM member the bridge calls- (149 rows below, covering `com.rs`, the modelling DSL and the renderer) was+ (165 rows below, covering `com.rs`, the modelling DSL and the renderer) was looked up on help.solidworks.com. The newest is **SOLIDWORKS 2017 (revision- 25)**: `IFeatureManager::FeatureCut4` and `ISldWorks::GetCurrentLicenseType`.+ 25)**: `IFeatureManager::FeatureCut4`, `ISldWorks::GetCurrentLicenseType` and+ `IModelDocExtension::SelectByRay` (a fallback rung the bridge skips below 2017). Nothing is newer than 2021, so nothing needs a fallback on 2021. - **[measured] Tested on SOLIDWORKS 2026, live.** On 2026-10-14 the modelling ops of issues #1 and #2 ran on SOLIDWORKS 2026 (major 34, 3DEXPERIENCE@@ -95,13 +96,18 @@ available in SOLIDWORKS 2021. | `ISelectionMgr::CreateSelectData` | 2004 FCS, Revision Number 12 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iselectionmgr~createselectdata.html) | | `IMathUtility::CreateTransform` | 2001 FCS, Revision Number 9.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imathutility~createtransform.html) | | `IModelDocExtension::CustomPropertyManager` | 2007 FCS, Revision Number 15.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~custompropertymanager.html) |+| `IModelDocExtension::DeleteSelection2` | 2006 SP1, Revision Number 14.1 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~deleteselection2.html) | | `IMassProperty::Density` | 2003 SP2, Revision Number 11.2 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imassproperty~density.html) | | `IModelView::DisplayMode` | 2004 SP1, Revision Number 12.1 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodelview~displaymode.html) |+| `ISldWorks::DocumentVisible` | — (no Availability line: an original-API member) | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~documentvisible.html) | | `IInterferenceDetectionMgr::Done` | 2007 FCS, Revision Number 15.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iinterferencedetectionmgr~done.html) | | `IModelDoc2::EditRebuild3` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~editrebuild3.html) | | `IEquationMgr::Equation` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iequationmgr~equation.html) |+| `ICurve::Evaluate2` | 2007 SP5, Revision Number 15.5 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.icurve~evaluate2.html) |+| `ISurface::EvaluateAtPoint` | — (no Availability line: an original-API member) | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isurface~evaluateatpoint.html) | | `ISldWorks::ExitApp` | — (no Availability line: an original-API member) | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~exitapp.html) | | `IModelDoc2::Extension` | 2003 FCS, Revision Number 11.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~extension.html) |+| `IFace2::FaceInSurfaceSense` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iface2~faceinsurfacesense.html) | | `IFeatureManager::FeatureCircularPattern4` | 2013 FCS, Revision Number 21.0 | yes | obsolete → IFeatureManager::FeatureCircularPattern5 | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeaturemanager~featurecircularpattern4.html) | | `IFeatureManager::FeatureCut4` | 2017 FCS, Revision Number 25.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeaturemanager~featurecut4.html) | | `IFeatureManager::FeatureExtrusion3` | 2014 FCS, Revision Number 22.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeaturemanager~featureextrusion3.html) |@@ -115,11 +121,15 @@ available in SOLIDWORKS 2021. | `ICustomPropertyManager::Get4` | 2011 SP4, Revision Number 19.4 | yes | obsolete → ICustomPropertyManager::Get5 | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.icustompropertymanager~get4.html) | | `IPartDoc::GetBodies2` | 2003 FCS, Revision Number 11.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ipartdoc~getbodies2.html) | | `IBody2::GetBodyBox` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ibody2~getbodybox.html) |+| `IEdge::GetClosestPointOn` | — (no Availability line: an original-API member) | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iedge~getclosestpointon.html) |+| `IFace2::GetClosestPointOn` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iface2~getclosestpointon.html) | | `IAssemblyDoc::GetComponentCount` | 2006 FCS, Revision Number 14 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iassemblydoc~getcomponentcount.html) | | `IAssemblyDoc::GetComponents` | 2006 FCS, Revision Number 14 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iassemblydoc~getcomponents.html) | | `IModelDoc2::GetConfigurationNames` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~getconfigurationnames.html) | | `IEquationMgr::GetCount` | 99, datecode 1999207 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iequationmgr~getcount.html) | | `ISldWorks::GetCurrentLicenseType` | 2017 FCS, Revision Number 25.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~getcurrentlicensetype.html) |+| `IEdge::GetCurve` | — (no Availability line: an original-API member) | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iedge~getcurve.html) |+| `IEdge::GetCurveParams2` | 2000 FCS, Revision Number 8.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iedge~getcurveparams2.html) | | `ISldWorks::GetDocumentCount` | 2009 FCS, Revision Number 17.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~getdocumentcount.html) | | `IPackAndGo::GetDocumentSaveToNames` | 2011 FCS, Revision Number 19.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ipackandgo~getdocumentsavetonames.html) | | `ISldWorks::GetDocumentTemplate` | 2001Plus SP4, Revision Number 10.4 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~getdocumenttemplate.html) |@@ -150,8 +160,11 @@ available in SOLIDWORKS 2021. | `ISldWorks::GetProcessID` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.isldworks~getprocessid.html) | | `IModelDoc2::GetSaveFlag` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~getsaveflag.html) | | `ISelectionMgr::GetSelectedObject6` | 2006 FCS, Revision Number 14 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iselectionmgr~getselectedobject6.html) |+| `ISelectionMgr::GetSelectedObjectCount2` | 2006 FCS, Revision Number 14.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iselectionmgr~getselectedobjectcount2.html) | | `IComponent2::GetSuppression` | 2001Plus FCS, Revision Number 10.0 | yes | obsolete → IComponent2::GetSuppression2 | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.icomponent2~getsuppression.html) |+| `IFace2::GetSurface` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iface2~getsurface.html) | | `IModelDoc2::GetTitle` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~gettitle.html) |+| `IEdge::GetTwoAdjacentFaces2` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iedge~gettwoadjacentfaces2.html) | | `IModelDoc2::GetType` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~gettype.html) | | `IFeature::GetTypeName2` | 2008 FCS, Revision Number 16.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeature~gettypename2.html) | | `IModelDocExtension::GetUserPreferenceInteger` | 2009 FCS, Revision Number 17.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~getuserpreferenceinteger.html) |@@ -174,6 +187,7 @@ available in SOLIDWORKS 2021. | `IInterferenceDetectionMgr::MakeInterferingPartsTransparent` | 2007 FCS, Revision Number 15.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iinterferencedetectionmgr~makeinterferingpartstransparent.html) | | `ISelectData::Mark` | 2004 FCS, Revision Number 12 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iselectdata~mark.html) | | `IMassProperty::Mass` | 2003 SP2, Revision Number 11.2 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imassproperty~mass.html) |+| `IModelDocExtension::MultiSelect2` | 2012 SP04, Revision Number 20.4 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~multiselect2.html) | | `IFeature::Name` | — (no Availability line: an original-API member) | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeature~name.html) | | `IBody2::Name` | 2005 FCS, Revision Number 13 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ibody2~name.html) | | `IConfiguration::Name` | — (no Availability line: an original-API member) | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.iconfiguration~name.html) |@@ -193,9 +207,11 @@ available in SOLIDWORKS 2021. | `IModelDoc2::SaveBMP` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~savebmp.html) | | `IModelDocExtension::SavePackAndGo` | 2011 FCS, Revision Number 19.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~savepackandgo.html) | | `IModelView::Scale2` | 99, datecode 1999207 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodelview~scale2.html) |+| `IFeature::Select2` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ifeature~select2.html) | | `IEntity::Select4` | 2004 FCS, Revision Number 12.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ientity~select4.html) | | `IModelDoc2::SelectByID` | — (no Availability line: an original-API member) | yes | obsolete → IModelDocExtension::SelectByID2 | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~selectbyid.html) | | `IModelDocExtension::SelectByID2` | 2005 FCS, Revision Number 13.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~selectbyid2.html) |+| `IModelDocExtension::SelectByRay` | 2017 FCS, Revision Number 25.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~selectbyray.html) | | `IModelDoc2::SelectionManager` | 2001Plus FCS, Revision Number 10 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~selectionmanager.html) | | `IPartDoc::SetMaterialPropertyName2` | 2005 FCS, Revision Number 13.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ipartdoc~setmaterialpropertyname2.html) | | `IPackAndGo::SetSaveToName` | 2011 FCS, Revision Number 19.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ipackandgo~setsavetoname.html) |@@ -218,6 +234,7 @@ available in SOLIDWORKS 2021. | `IModelDocExtension::ViewDisplayRealView` | 2005 FCS, Revision Number 13.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldocextension~viewdisplayrealview.html) | | `IModelDoc2::ViewZoomtofit2` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~viewzoomtofit2.html) | | `IBody2::Visible` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.ibody2~visible.html) |+| `IModelDoc2::Visible` | 2001Plus FCS, Revision Number 10.0 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imodeldoc2~visible.html) | | `IMassProperty::Volume` | 2003 SP2, Revision Number 11.2 | yes | | [help](https://help.solidworks.com/2024/english/api/sldworksapi/solidworks.interop.sldworks~solidworks.interop.sldworks.imassproperty~volume.html) | Enum values the DSL passes are read from the published swconst pages (cited
install.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.10.5",+ "version": "0.10.6", "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+14@@ -40,6 +40,20 @@ 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. - `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+45−4@@ -30,13 +30,38 @@ Parent skill: **solidworks-bridge** (test batteries, `swx_selftest {"level":"write"}`, batch exports, many `new_part` scripts) are what fill the machine. +## The measured curve, and what causes it++- [measured] 2026-10-14 (hosted bridge 0.10.5, a freshly started SOLIDWORKS 2026): 12 times+ `new_part` plus a 20 x 20 x 10 mm box, then `swx_reset_workspace`, sampled after every+ close with `swx_status.memory` and `GetDocumentCount`. SOLIDWORKS's private bytes went+ **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.+ ## The rules 1. **Read memory before a long run, and between its steps.** `swx_status` reports a- `memory` block (commit used, limit and available; SOLIDWORKS's own committed bytes; the- largest holders). [unmeasured: the bridge reports it from 0.10.5; not yet read on a- live machine] If commit- available is low before you start, do not start: tell the user what holds the memory.+ `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@@ -50,6 +75,9 @@ Parent skill: **solidworks-bridge** however much it holds. Report the largest holders with their sizes and let the user decide. Restarting a long-running Fusion or browser is the user's call. 5. **Never change the page file or SOLIDWORKS settings** to make room. Those are the user's.+ (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@@ -59,6 +87,19 @@ Parent skill: **solidworks-bridge** 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`. ## Diagnosing by hand (read-only)
src/comhost/com.rs+217−19@@ -2061,7 +2061,7 @@ impl ComBackend { // A script that starts by creating its own document touches nothing // that exists yet. if let Ok(ops) = crate::script::parse(&args.get("script").cloned().unwrap_or(Value::Null)) {- if matches!(ops.first(), Some(crate::script::Op::NewPart | crate::script::Op::NewAssembly)) {+ if matches!(ops.first(), Some(crate::script::Op::NewPart { .. } | crate::script::Op::NewAssembly { .. })) { return None; } }@@ -2608,6 +2608,7 @@ impl Backend for ComBackend { // v0.3 "new_document" => self.cmd_new_document(args),+ "restore_document_visible" => self.cmd_restore_document_visible(args), "activate_document" => self.cmd_activate_document(args), "close_all_documents" => self.cmd_close_all_documents(args), "document_info" => self.cmd_document_info(args),@@ -2732,19 +2733,90 @@ impl ComBackend { if !std::path::Path::new(&template).exists() { return Err(HostError::msg(format!("template not found on disk: {template}"))); }- let doc = sw- .call(- "NewDocument",- &[&Var::bstr(&template), &Var::i4(0), &Var::r8(0.0), &Var::r8(0.0)],- )?+ let visible = args.get("visible").and_then(|v| v.as_bool()).unwrap_or(true);+ if visible {+ let doc = Self::new_document_from(&sw, &template)?;+ return Ok(json!({"created": Self::doc_info(&doc), "template": template, "type": kind}));+ }+ // HIDDEN (0.10.6). MEASURED 2026-10-14: every created-and-closed+ // document cost SOLIDWORKS ~83 MB of commit and ~125 handles for good,+ // with GetDocumentCount back at 0 — the leading hypothesis is the+ // window and graphics context each visible document gets.+ // ISldWorks::DocumentVisible(Visible, Type) "allows the application to+ // control the display of a document in a window upon creation or+ // retrieval" (help.solidworks.com, ISldWorks::DocumentVisible), and+ // IModelDoc2::Visible's own remarks say hiding a document does NOT+ // release its user-interface resources — "open the document invisibly+ // using ISldWorks::DocumentVisible" instead. It is a SESSION setting:+ // it is put back to true immediately after NewDocument, on every path,+ // the error path included, and the reply says whether that worked.+ let b = crate::memory::hidden_bracket(+ |visible| {+ let r = sw.call("DocumentVisible", &[&Var::boolean(visible), &Var::i4(type_id)]).map(|_| ());+ if r.is_err() && visible {+ std::thread::sleep(std::time::Duration::from_millis(300));+ }+ r+ },+ || Self::new_document_from(&sw, &template),+ 3,+ );+ let mut visibility = json!({+ "requested": false,+ "hiddenWith": "ISldWorks::DocumentVisible",+ "documentVisibleSet": b.set_ok,+ "documentVisibleRestored": b.restored,+ });+ match b.made {+ Ok(doc) => {+ visibility["visibleReadBack"] = json!(doc.flag("Visible", &[]));+ // ITS FILE, BY THE OBJECT WE HOLD. A hidden document is not+ // necessarily the ACTIVE one, so the server's usual "save the+ // active document into the scratch folder" could name somebody+ // else's: the scratch file is written here, through this Disp.+ if let Some(path) = args.get("scratchPath").and_then(|v| v.as_str()).filter(|p| !p.is_empty()) {+ let code = Self::save_as3(&doc, path, false);+ visibility["scratchSaved"] = json!(matches!(code, Ok(0)));+ }+ Ok(json!({"created": Self::doc_info(&doc), "template": template, "type": kind, "visibility": visibility}))+ }+ Err(e) => Err(e.with_data(json!({"visibility": visibility}))),+ }+ }++ fn new_document_from(sw: &Disp, template: &str) -> Result<Disp, HostError> {+ sw.call("NewDocument", &[&Var::bstr(template), &Var::i4(0), &Var::r8(0.0), &Var::r8(0.0)])? .as_dispatch() .map(Disp)- .ok_or_else(|| {- HostError::msg(format!(- "SOLIDWORKS created no document from {template} (is the template valid?)"- ))- })?;- Ok(json!({"created": Self::doc_info(&doc), "template": template, "type": kind}))+ .ok_or_else(|| HostError::msg(format!("SOLIDWORKS created no document from {template} (is the template valid?)")))+ }++ /// PUT THE SESSION SETTING BACK: DocumentVisible(true, type), retried —+ /// left false, every document the USER opens next would be invisible.+ fn restore_document_visible(sw: &Disp, type_id: i32) -> bool {+ for attempt in 0..3u64 {+ if sw.call("DocumentVisible", &[&Var::boolean(true), &Var::i4(type_id)]).is_ok() {+ return true;+ }+ std::thread::sleep(std::time::Duration::from_millis(200 * (attempt + 1)));+ }+ false+ }++ /// `restore_document_visible` — the server's second chance when a hidden+ /// creation reported the setting NOT restored. Only ever sets it TRUE.+ fn cmd_restore_document_visible(&mut self, args: &Value) -> Result<Value, HostError> {+ let kind = args.get("type").and_then(|v| v.as_str()).unwrap_or("part");+ let type_id = Self::type_id_from_name(kind)?;+ let sw = self.app(false, true)?;+ let restored = Self::restore_document_visible(&sw, type_id);+ if !restored {+ return Err(HostError::msg(format!(+ "ISldWorks::DocumentVisible(true, {kind}) failed three times — documents of that type \+ may open INVISIBLE in this session until SOLIDWORKS restarts"+ )));+ }+ Ok(json!({"documentVisibleRestored": true, "type": kind})) } /// `swx_activate_document`. Not provable live in this session (SOLIDWORKS@@ -2759,6 +2831,13 @@ impl ComBackend { .filter(|s| !s.is_empty()) .ok_or_else(|| HostError::msg("name (the document title, as swx_docs reports it) is required"))?; let sw = self.app(false, true)?;+ // `visible:true` (0.10.6): a hidden document is made visible first —+ // attempted and read back (UNMEASURED; see make_visible).+ let visibility = if args.get("visible").and_then(|v| v.as_bool()) == Some(true) {+ self.resolve(&json!({"docName": name})).ok().map(|d| Self::make_visible(&d))+ } else {+ None+ }; let mut errors = 0i32; // ActivateDoc3(Name, UseUserPreferences, Option, byref Errors). // swRebuildOnActivation_e: 1 = swDontRebuildActiveDoc. 0 is@@ -2782,7 +2861,7 @@ impl ComBackend { the titles that are actually open" )) })?;- Ok(json!({"activeDocument": Self::doc_info(&doc), "errors": errors}))+ Ok(json!({"activeDocument": Self::doc_info(&doc), "errors": errors, "visibility": visibility})) } /// Enumerate the open documents, and NEVER return an empty list the@@ -4290,15 +4369,23 @@ impl ComBackend { fn cmd_show(&mut self, args: &Value) -> Result<Value, HostError> { let doc = self.resolve(args)?; let title = doc.string("GetTitle", &[]).unwrap_or_default();+ // A HIDDEN document (0.10.6) is made visible first — attempted and read+ // back, never assumed: the help page for ISldWorks::DocumentVisible+ // says a document created invisibly "cannot" be made visible with+ // IModelDoc2::Visible (close it and reopen it instead). UNMEASURED.+ let visibility = Self::make_visible(&doc); let mut errors = 0i32; let activated = if args.get("activate").and_then(|v| v.as_bool()).unwrap_or(true) { let sw = self.app(false, true)?;+ // swRebuildOnActivation_e 1 = swDontRebuildActiveDoc (0 is+ // swUserDecision, which PROMPTS — fixed here in 0.10.6 as it was in+ // activate in 0.10.2). sw.call( "ActivateDoc3", &[ &Var::bstr(&title), &Var::boolean(false),- &Var::i4(0),+ &Var::i4(1), &Var::byref_i4(&mut errors as *mut i32), ], )@@ -4313,9 +4400,31 @@ impl ComBackend { "activated": activated, "zoomedToFit": zoomed, "activateErrors": errors,+ "visibility": visibility, })) } + /// IModelDoc2::Visible: read; if false, put true and read again.+ fn make_visible(doc: &Disp) -> Value {+ let before = doc.flag("Visible", &[]);+ if before != Some(false) {+ return json!({"wasVisible": before, "madeVisible": false});+ }+ let put = doc.put("Visible", &Var::boolean(true)).is_ok();+ let after = doc.flag("Visible", &[]);+ json!({+ "wasVisible": false,+ "putVisible": put,+ "visibleAfter": after,+ "madeVisible": after == Some(true),+ "note": if after == Some(true) { Value::Null } else {+ json!("SOLIDWORKS kept this document hidden: a document created invisibly cannot be made \+ visible (help.solidworks.com, ISldWorks::DocumentVisible). Save it and reopen it \+ (swx_save_as, then swx_open) to see it.")+ },+ })+ }+ /// `swx_render_image` (0.9.18) — MEASURED 2026-10-05 on the laptop (window /// minimized): 4K in 11.6 s, every setting read back restored, the four /// swconst ids resolved. `shadows:true` did not appear in applied/restored —@@ -5338,6 +5447,10 @@ impl ComBackend { let step = i + 1; let made_before = created.as_ref().and_then(|c| c.get("created").cloned()); let detail = self.run_op(op, &pinned, &mut st).map_err(|e| {+ // The failing step's structured data (a hidden new_part's+ // `visibility`, an HRESULT) and its code travel with the error.+ let data = e.data.clone();+ let code = e.code.clone(); let err = HostError::detailed( format!("step {step} ({}) failed: {}", op.name(), e.message), e.detail.unwrap_or_else(|| {@@ -5350,17 +5463,20 @@ impl ComBackend { ) }), );- match &made_before {+ let mut err = match &made_before { Some(doc) => err.with_created(doc.clone()), None => err,- }+ };+ err.data = data;+ err.code = code;+ err })?; if let Some(f) = detail.get("feature").and_then(|f| f.as_str()) { if op.makes_feature() { st.last_feature = Some(f.to_string()); } }- if matches!(op, Op::NewPart | Op::NewAssembly) {+ if matches!(op, Op::NewPart { .. } | Op::NewAssembly { .. }) { // A new document is active and starts clean; whatever the user had // active before is still put back only if the script moved it. let keep = st.restore_active.take();@@ -5371,6 +5487,7 @@ impl ComBackend { .and_then(|c| c.get("title")) .and_then(|t| t.as_str()) .map(String::from);+ st.created_here = pinned.clone(); created = Some(detail.clone()); } results.push(json!({"step": step, "op": op.name(), "result": detail}));@@ -5416,8 +5533,62 @@ impl ComBackend { use crate::script::Op; let target = Self::doc_arg(pinned); match op {- Op::NewPart => self.cmd_new_document(&json!({"type": "part"})),- Op::NewAssembly => self.cmd_new_document(&json!({"type": "assembly"})),+ Op::NewPart { visible } => self.cmd_new_document(&json!({"type": "part", "visible": visible})),+ Op::NewAssembly { visible } => self.cmd_new_document(&json!({"type": "assembly", "visible": visible})),++ Op::ClearPart => {+ // EVERY FEATURE AFTER THE ORIGIN, deleted — in a part the BRIDGE+ // created, and nowhere else: the host's own record decides,+ // whatever the server sent (0.10.6). N geometry checks then cost+ // one document instead of N.+ let doc = self.resolve(&target)?;+ let title = doc.string("GetTitle", &[]).unwrap_or_default();+ if doc.int("GetType", &[]) != Some(DOC_PART) {+ return Err(HostError::msg(format!("clear_part works on a part; '{title}' is not one")));+ }+ if st.created_here.as_deref() != Some(title.as_str()) && !self.host_owns(&doc) {+ return Err(HostError::coded(+ "clear_part_not_owned",+ format!("clear_part deletes features only in a part this bridge created; '{title}' is not one"),+ ));+ }+ self.ensure_active(&doc, st)?;+ let after_origin = Self::features_after_origin(&doc)?;+ let names: Vec<String> = after_origin.iter().filter_map(|f| f.string("Name", &[])).collect();+ let mut deleted = 0usize;+ if !after_origin.is_empty() {+ let _ = Self::clear_selection(&doc);+ for f in &after_origin {+ let _ = f.call("Select2", &[&Var::boolean(true), &Var::i4(0)]);+ }+ // swDeleteSelectionOptions_e: swDelete_Children 1 | swDelete_Absorbed 2+ // (help.solidworks.com, swconst) — the sketches a feature+ // absorbed, and anything built on it, go too. DeleteSelection2+ // (2006 SP1) "does not ask the user to confirm the deletion".+ let ext = doc.obj("Extension", &[])?.ok_or_else(|| HostError::msg("no ModelDocExtension"))?;+ let ok = ext.call("DeleteSelection2", &[&Var::i4(1 | 2)])?.as_bool().unwrap_or(false);+ let _ = Self::clear_selection(&doc);+ let left = Self::features_after_origin(&doc)?;+ if !ok || !left.is_empty() {+ let still: Vec<String> = left.iter().filter_map(|f| f.string("Name", &[])).collect();+ return Err(HostError::msg(format!(+ "clear_part could not delete every feature (DeleteSelection2 returned {ok}; still there: {})",+ still.join(", ")+ )));+ }+ deleted = names.len();+ }+ let bodies = doc.call("GetBodies2", &[&Var::i4(0), &Var::boolean(false)]).map(|b| b.as_array().len()).unwrap_or(0);+ // The script's own memory of planes, axes and sketches is about+ // features that no longer exist.+ let keep_active = st.restore_active.take();+ let created_here = st.created_here.take();+ *st = ScriptState::default();+ st.restore_active = keep_active;+ st.created_here = created_here;+ Ok(json!({"cleared": title, "featuresDeleted": deleted, "deleted": names, "bodiesLeft": bodies,+ "route": "DeleteSelection2(swDelete_Children | swDelete_Absorbed)"}))+ } Op::Sketch { plane, entities, .. } => { let doc = self.resolve(&target)?;@@ -6081,6 +6252,31 @@ impl ComBackend { .unwrap_or(false)) } + /// The TOP-LEVEL features after the origin (FirstFeature / GetNextFeature;+ /// the origin is the `OriginProfileFeature`): what a script built.+ fn features_after_origin(doc: &Disp) -> Result<Vec<Disp>, HostError> {+ let mut f = doc.obj("FirstFeature", &[])?;+ let mut seen_origin = false;+ let mut out = Vec::new();+ let mut guard = 0;+ while let Some(feat) = f {+ guard += 1;+ if guard > 100_000 {+ break;+ }+ if seen_origin {+ out.push(feat.clone());+ } else if feat.string("GetTypeName2", &[]).as_deref() == Some("OriginProfileFeature") {+ seen_origin = true;+ }+ f = feat.obj("GetNextFeature", &[]).ok().flatten();+ }+ if !seen_origin {+ return Err(HostError::msg("this part has no Origin in its feature tree, so clear_part cannot tell what the script built"));+ }+ Ok(out)+ }+ /// The tree's last feature's name (FirstFeature / GetNextFeature). fn last_feature_name(doc: &Disp) -> Option<String> { let mut f = doc.obj("FirstFeature", &[]).ok().flatten()?;@@ -6857,6 +7053,8 @@ struct PendingSketch { /// and the document to re-activate when it ends. #[derive(Default)] struct ScriptState {+ /// The document THIS script's new_part made (clear_part may act on it).+ created_here: Option<String>, last_feature: Option<String>, planes: std::collections::HashMap<String, String>, axes: std::collections::HashMap<crate::script::Axis, (String, f64)>,
src/comhost/host.rs+7@@ -150,6 +150,11 @@ pub const READ_ONLY_COMMANDS: &[&str] = &[ // the result is the bridge's own, and the host records it as such. "new_document", "import",+ // `restore_document_visible` (0.10.6) puts ISldWorks::DocumentVisible+ // back to TRUE for one document type — the second+ // chance after a hidden creation reported it was+ // not restored. It can only ever un-hide.+ "restore_document_visible", // `launch_shortcut` STARTS SOLIDWORKS, by invoking the shortcut the // 3DEXPERIENCE Platform installed. A fourth // exemption, and it needs its own justification@@ -837,6 +842,8 @@ pub const COMMANDS: &[&str] = &[ "eval", // v0.3 — the Fusion-parity surface. "new_document",+ // 0.10.6 — DocumentVisible put back after a hidden creation.+ "restore_document_visible", // 0.9.3 — has SOLIDWORKS finished starting? (the readiness gate) "startup_state", "activate_document",
src/comhost/mock.rs+206−7@@ -117,6 +117,9 @@ struct MockDoc { /// The simulated solid model (0.10.0): features, bounding box, volume, /// appearance — see `mock_model`. model: Map<String, Value>,+ /// Created while ISldWorks::DocumentVisible was false for its type (0.10.6):+ /// no window. Per the help page, IModelDoc2::Visible cannot show it later.+ visible: bool, } impl MockDoc {@@ -142,6 +145,7 @@ impl MockDoc { "importTemp": self.import_temp, "tempPath": self.temp_path, "model": Value::Object(self.model.clone()),+ "visible": self.visible, }) } @@ -181,6 +185,7 @@ impl MockDoc { import_temp: v.get("importTemp").and_then(|x| x.as_bool()).unwrap_or(false), temp_path: v.get("tempPath").and_then(|x| x.as_str()).map(String::from), model: v.get("model").and_then(|x| x.as_object()).cloned().unwrap_or_default(),+ visible: v.get("visible").and_then(|x| x.as_bool()).unwrap_or(true), }) } @@ -202,6 +207,7 @@ impl MockDoc { import_temp: false, temp_path: None, model: Map::new(),+ visible: true, } } }@@ -275,6 +281,42 @@ fn fault(name: &str) -> bool { crate::scratch::root().join(name).exists() } +/// ISldWorks::DocumentVisible, simulated (0.10.6): a SESSION setting per+/// document type, persisted in the data root like SOLIDWORKS keeps it for the+/// life of the process; every call is appended to `document-visible.jsonl` so a+/// test can read the order (false, NewDocument, true).+fn doc_visible_path() -> std::path::PathBuf {+ crate::scratch::root().join("mock-documentvisible.json")+}++fn doc_visible_off(kind: &str) -> bool {+ std::fs::read_to_string(doc_visible_path())+ .ok()+ .and_then(|t| serde_json::from_str::<Value>(&t).ok())+ .and_then(|v| v.get(kind).and_then(|b| b.as_bool()))+ .map(|visible| !visible)+ .unwrap_or(false)+}++fn document_visible(kind: &str, visible: bool) -> Result<(), HostError> {+ let log = crate::scratch::root().join("document-visible.jsonl");+ let mut t = std::fs::read_to_string(&log).unwrap_or_default();+ let fails = visible && (fault("mock-docvisible-restore-fails") || fault("mock-docvisible-restore-fails-always"));+ t.push_str(&json!({"visible": visible, "type": kind, "ok": !fails}).to_string());+ t.push('\n');+ let _ = std::fs::write(&log, t);+ if fails {+ return Err(HostError::msg("simulated: DocumentVisible failed"));+ }+ let mut state = std::fs::read_to_string(doc_visible_path())+ .ok()+ .and_then(|t| serde_json::from_str::<Value>(&t).ok())+ .unwrap_or_else(|| json!({}));+ state[kind] = json!(visible);+ let _ = std::fs::write(doc_visible_path(), state.to_string());+ Ok(())+}+ /// A fresh script run on the simulated install. Selection behaves as the first /// live run on SOLIDWORKS 2026 measured (the VARIANT-array MultiSelect2 selects /// nothing); `mock-select-dead-<rung>` kills a rung of the ladder too.@@ -770,6 +812,23 @@ impl MockBackend { /// Does this host believe it created the document at `key`? Path where there /// is one, title otherwise — the same identity rule everywhere else.+ /// What IModelDoc2::Visible = true does to a document, as the help page+ /// documents it: a document created invisibly STAYS hidden.+ fn make_visible_report(visible: bool) -> Value {+ if visible {+ return json!({"wasVisible": true, "madeVisible": false});+ }+ json!({+ "wasVisible": false,+ "putVisible": true,+ "visibleAfter": false,+ "madeVisible": false,+ "note": "SOLIDWORKS kept this document hidden: a document created invisibly cannot be made visible \+ (help.solidworks.com, ISldWorks::DocumentVisible). Save it and reopen it (swx_save_as, then \+ swx_open) to see it.",+ })+ }+ fn host_owns(&self, key: &str) -> bool { if self.created_session != Some(self.session()) { return false;@@ -799,7 +858,7 @@ impl MockBackend { return self.resolve(&json!({"docName": t})).ok(); } if let Ok(ops) = crate::script::parse(&args.get("script").cloned().unwrap_or(Value::Null)) {- if matches!(ops.first(), Some(crate::script::Op::NewPart | crate::script::Op::NewAssembly)) {+ if matches!(ops.first(), Some(crate::script::Op::NewPart { .. } | crate::script::Op::NewAssembly { .. })) { return None; } }@@ -1047,6 +1106,15 @@ impl Backend for MockBackend { } fn call(&mut self, cmd: &str, args: &Value) -> Result<Value, HostError> {+ // WHAT REACHED THE SIMULATED SOLIDWORKS, when a test asks+ // (`mock-log-commands`): how a test proves a verb never called it.+ if fault("mock-log-commands") {+ let f = crate::scratch::root().join("mock-commands.log");+ let mut t = std::fs::read_to_string(&f).unwrap_or_default();+ t.push_str(cmd);+ t.push('\n');+ let _ = std::fs::write(&f, t);+ } let out = self.dispatch(cmd, args); // The simulated APPLICATION's session outlives the com host, because a // real one does. Written after every command so a respawn resumes from@@ -2465,6 +2533,58 @@ impl MockBackend { "new_document" => { self.running = true; let kind = args.get("type").and_then(|v| v.as_str()).unwrap_or("part");+ let visible = args.get("visible").and_then(|v| v.as_bool()).unwrap_or(true);+ if !visible {+ // THE BRACKET: off, create, back on — on every path.+ let canonical = match kind.trim().to_lowercase().as_str() {+ "assembly" | "asm" | "sldasm" => "assembly",+ "drawing" | "drw" | "slddrw" => "drawing",+ _ => "part",+ };+ let mut inner = args.clone();+ inner["visible"] = json!(true);+ let b = crate::memory::hidden_bracket(+ |visible| document_visible(canonical, visible),+ || {+ if fault("mock-newdoc-fails") {+ return Err(HostError::msg(+ "simulated: SOLIDWORKS created no document (NewDocument failed inside the hidden bracket)",+ ));+ }+ self.call("new_document", &inner)+ },+ 3,+ );+ let mut visibility = json!({+ "requested": false,+ "hiddenWith": "ISldWorks::DocumentVisible",+ "documentVisibleSet": b.set_ok,+ "documentVisibleRestored": b.restored,+ });+ return match b.made {+ Ok(mut r) => {+ let key = r.pointer("/created/title").and_then(|t| t.as_str()).map(|t| format!("untitled:{t}"));+ let read_back = key.as_deref().and_then(|k| self.find(k)).map(|d| d.visible);+ visibility["visibleReadBack"] = json!(read_back);+ // Its scratch file, written through the document itself.+ if let (Some(path), Some(title)) = (+ args.get("scratchPath").and_then(|v| v.as_str()).filter(|p| !p.is_empty()),+ r.pointer("/created/title").and_then(|t| t.as_str()).map(String::from),+ ) {+ let saved = self.call("save_as", &json!({"docName": title, "out": path, "copy": false}));+ visibility["scratchSaved"] = json!(saved.is_ok());+ if let Ok(s) = saved {+ if let Some(doc) = s.get("doc").cloned() {+ r["created"] = doc;+ }+ }+ }+ r["visibility"] = visibility;+ Ok(r)+ }+ Err(e) => Err(e.with_data(json!({"visibility": visibility}))),+ };+ } // EXHAUSTION, as the real thing reports it: SOLIDWORKS accepts // the call and hands back nothing. Observed live on 2026-09-03 // for BOTH the part and the assembly template, at 17.4 GB and@@ -2498,11 +2618,41 @@ impl MockBackend { let mut doc = MockDoc::opened(key.clone(), doc_type, "Default".into()); doc.title = title; doc.saved = false;+ // A document is created hidden whenever DocumentVisible is false+ // for its type — asked for, or LEFT false by a failed restore.+ doc.visible = !doc_visible_off(doc_type);+ // THE MEASURED GROWTH (2026-10-14): every document created costs+ // SOLIDWORKS memory it keeps after the close. The simulated+ // machine counts creations; `sldworks.json`'s+ // `growthPerDocumentMB` turns the count into private bytes.+ if let Some(d) = fake_dir() {+ let f = d.join("documents-created");+ let n = std::fs::read_to_string(&f).ok().and_then(|t| t.trim().parse::<u64>().ok()).unwrap_or(0);+ let _ = std::fs::write(&f, (n + 1).to_string());+ }+ let hidden = !doc.visible; self.docs.push(doc);- self.active = Some(key.clone());+ // A hidden document is not made the active one (UNMEASURED live;+ // modelled conservatively).+ if !hidden {+ self.active = Some(key.clone());+ } Ok(json!({"created": self.doc_info(&key), "template": template, "type": kind})) } + "restore_document_visible" => {+ let kind = args.get("type").and_then(|v| v.as_str()).unwrap_or("part");+ if fault("mock-docvisible-restore-fails-always") {+ return Err(HostError::msg(format!(+ "ISldWorks::DocumentVisible(true, {kind}) failed three times — documents of that type may \+ open INVISIBLE in this session until SOLIDWORKS restarts"+ )));+ }+ let _ = std::fs::remove_file(crate::scratch::root().join("mock-docvisible-restore-fails"));+ document_visible(kind, true)?;+ Ok(json!({"documentVisibleRestored": true, "type": kind}))+ }+ "activate_document" => { self.require_running()?; let name = args@@ -2523,7 +2673,12 @@ impl MockBackend { )) })?; self.active = Some(key.clone());- Ok(json!({"activeDocument": self.doc_info(&key), "errors": 0}))+ 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))+ } else {+ None+ };+ Ok(json!({"activeDocument": self.doc_info(&key), "errors": 0, "visibility": visibility})) } "close_all_documents" => {@@ -3181,12 +3336,14 @@ impl MockBackend { let path = self.resolve(args)?; self.active = Some(path.clone()); let info = self.doc_info(&path);+ let visibility = self.find(&path).map(|d| Self::make_visible_report(d.visible)); Ok(json!({ "document": info.get("title").cloned().unwrap_or(Value::Null), "doc": info, "activated": args.get("activate").and_then(|v| v.as_bool()).unwrap_or(true), "zoomedToFit": true, "activateErrors": 0,+ "visibility": visibility, })) } @@ -3728,14 +3885,54 @@ impl MockBackend { let target = pin_arg(&pinned); let detail: Result<Value, HostError> = (|| -> Result<Value, HostError> { Ok(match op {- Op::NewPart | Op::NewAssembly => {- let kind = if matches!(op, Op::NewPart) { "part" } else { "assembly" };- let r = self.call("new_document", &json!({"type": kind}))?;+ Op::NewPart { visible } | Op::NewAssembly { visible } => {+ let kind = if matches!(op, Op::NewPart { .. }) { "part" } else { "assembly" };+ let r = self.call("new_document", &json!({"type": kind, "visible": visible}))?; created = r.get("created").cloned().unwrap_or(Value::Null); pinned = created.get("title").and_then(|t| t.as_str()).map(String::from);+ // The real backend activates its target before a+ // selection (ensure_active) — a HIDDEN new part is+ // 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);+ }+ } run = fresh_run(); r }+ Op::ClearPart => {+ // Only in a part the BRIDGE created — the host's own+ // record decides (or the part this script made).+ let path = self.resolve(&target)?;+ let made_here = created.get("title").and_then(|t| t.as_str())+ .map(|t| self.find(&path).map(|d| d.title == t).unwrap_or(false))+ .unwrap_or(false);+ if !made_here && !self.host_owns(&path) {+ let title = self.find(&path).map(|d| d.title.clone()).unwrap_or_default();+ return Err(HostError::coded(+ "clear_part_not_owned",+ format!("clear_part deletes features only in a part this bridge created; '{title}' is not one"),+ ));+ }+ let d = self.docs.iter_mut().find(|d| d.path == path).ok_or_else(|| HostError::msg("document vanished"))?;+ if d.doc_type != "part" {+ return Err(HostError::msg(format!("clear_part works on a part; '{}' is not one", d.title)));+ }+ let mut model: crate::comhost::mock_model::Model =+ serde_json::from_value(Value::Object(d.model.clone())).unwrap_or_default();+ let deleted: Vec<String> = model.features.iter().map(|f| f.name.clone()).collect();+ model.clear();+ d.bodies = model.bodies_json();+ d.dimensions = Map::new();+ if let Value::Object(m) = serde_json::to_value(&model).unwrap_or(Value::Null) {+ d.model = m;+ }+ d.dirty = true;+ run = fresh_run();+ json!({"cleared": d.title, "featuresDeleted": deleted.len(), "deleted": deleted, "bodiesLeft": 0,+ "route": "DeleteSelection2(swDelete_Children | swDelete_Absorbed)"})+ } Op::SetProp { name, value } => { let path = self.resolve(&target)?; if let Some(d) = self.docs.iter_mut().find(|d| d.path == path) {@@ -3801,7 +3998,9 @@ impl MockBackend { match detail { Ok(d) => steps.push(json!({"step": i + 1, "op": op.name(), "result": d})), Err(e) => {- let err = HostError::msg(format!("step {} ({}) failed: {}", i + 1, op.name(), e.message));+ let mut err = HostError::msg(format!("step {} ({}) failed: {}", i + 1, op.name(), e.message));+ err.data = e.data.clone();+ err.code = e.code.clone(); // A part made by this script's new_part before the // failing step still exists, and is the bridge's. failure = Some(if created.is_object() { err.with_created(created.clone()) } else { err });
src/comhost/mock_model.rs+10@@ -779,6 +779,16 @@ impl Model { } /// The bodies, in swx_inspect_bodies' shape.+ /// clear_part (0.10.6): every feature and body goes; the part-level colour+ /// and material stay (they are not features), and so do the name counters+ /// (whether SOLIDWORKS restarts its feature numbering is UNMEASURED, so the+ /// simulator assumes it does not).+ pub fn clear(&mut self) {+ self.bodies.clear();+ self.features.clear();+ self.face_appearances.clear();+ }+ pub fn bodies_json(&self) -> Vec<Value> { self.bodies .iter()
src/comhost/mod.rs+44@@ -314,6 +314,40 @@ impl ComHost { } } + /// RESPAWN THE CHILD, AND NOTHING ELSE (swx_com_respawn, 0.10.6): the+ /// child exits through its own loop (`__exit__`), so its backend is DROPPED+ /// and every COM reference it held is released by its own Release calls —+ /// not left to the ~6-minute COM ping rundown a killed process leaves.+ /// SOLIDWORKS is never called. A verb holding the host refuses (`Err`):+ /// pulling the host out from under a COM call is a different act.+ pub fn respawn(&self) -> Result<RespawnReport, String> {+ use std::sync::TryLockError;+ let mut guard = match self.live.try_lock() {+ Ok(g) => g,+ Err(TryLockError::WouldBlock) => return Err("a verb is in flight on the com host".into()),+ Err(TryLockError::Poisoned(g)) => g.into_inner(),+ };+ let old_pid = guard.as_ref().map(|l| l.pid);+ let mut graceful = old_pid.is_none();+ if let Some(live) = guard.as_mut() {+ let _ = live.stdin.write_all(b"{\"id\":0,\"cmd\":\"__exit__\"}\n");+ let _ = live.stdin.flush();+ let deadline = Instant::now() + Duration::from_secs(5);+ while Instant::now() < deadline {+ if matches!(live.child.try_wait(), Ok(Some(_))) {+ graceful = true;+ break;+ }+ std::thread::sleep(Duration::from_millis(50));+ }+ }+ kill(guard.take());+ self.sync_pid(&guard);+ *guard = Some(spawn_child().map_err(|e| e.message)?);+ self.sync_pid(&guard);+ Ok(RespawnReport { old_pid, new_pid: guard.as_ref().map(|l| l.pid), graceful_exit: graceful })+ }+ /// Ask the child to exit, then make sure of it. Idempotent, and it must NOT /// block on the request lock — ab's uninstall cascade has a 120s budget, but /// a 580s Pack and Go could be holding that lock.@@ -345,6 +379,16 @@ impl ComHost { } } +/// What a respawn did.+#[derive(Clone, Debug)]+pub struct RespawnReport {+ pub old_pid: Option<u32>,+ pub new_pid: Option<u32>,+ /// The old child left through its own loop (references released by Drop)+ /// rather than being killed.+ pub graceful_exit: bool,+}+ /// OS-level kill of the child by pid, used only when the request lock is held /// by a long COM call and we cannot reach the `Child` handle behind it. fn kill_by_pid(pid: u32) {
src/compat.rs+12−3@@ -37,6 +37,8 @@ pub fn major_of_year(year: u32) -> u32 { pub const GATED: &[(&str, u32)] = &[ ("IFeatureManager::FeatureCut4", 25), ("ISldWorks::GetCurrentLicenseType", 25),+ // The selection ladder's third rung (0.10.3) — skipped below 2017.+ ("IModelDocExtension::SelectByRay", 25), ("IFeatureManager::FeatureFillet3", 23), ("IFeatureManager::FeatureLinearPattern4", 23), ("ISldWorks::StartupProcessCompleted", 23),@@ -120,6 +122,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: "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 },@@ -246,8 +251,11 @@ pub fn op_variants(op: &crate::script::Op, result: &serde_json::Value) -> Vec<St use crate::script::{AppearanceTarget, Direction, EdgeSel, Op, PlaneRef}; let mut v: Vec<String> = Vec::new(); match op {- Op::NewPart => v.push("new_part".into()),- Op::NewAssembly => v.push("new_assembly".into()),+ Op::NewPart { visible: true } => v.push("new_part".into()),+ Op::NewPart { visible: false } => v.push("new_part.invisible".into()),+ Op::NewAssembly { visible: true } => v.push("new_assembly".into()),+ Op::NewAssembly { visible: false } => v.push("new_assembly.invisible".into()),+ Op::ClearPart => v.push("clear_part".into()), Op::Sketch { plane, entities, .. } => { v.push(match plane { PlaneRef::Named(n) => format!("sketch.plane.{}", plane_key(n)),@@ -513,7 +521,8 @@ mod tests { assert_eq!( unmeasured, [- "chamfer.allOfFeature", "extrude.both.throughAll", "fillet.createDefinition", "new_assembly", "rebuild",+ "chamfer.allOfFeature", "clear_part", "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", "sketch.offset.named", "sketch.offset.right.negative", "sketch.offset.right.positive", "sketch.plane.named",
src/launchos.rs+8−1@@ -1507,8 +1507,15 @@ impl LaunchOs for FakeOs { fn memory_of(&self, _pid: u32) -> (Option<u64>, Option<u32>) { let s = self.read_json("sldworks.json");+ // `growthPerDocumentMB` (0.10.6): the measured leak — every document+ // the simulated SOLIDWORKS created keeps that much after its close.+ let created = std::fs::read_to_string(self.dir.join("documents-created"))+ .ok()+ .and_then(|t| t.trim().parse::<u64>().ok())+ .unwrap_or(0);+ let growth = s.get("growthPerDocumentMB").and_then(|v| v.as_u64()).unwrap_or(0) * created; (- s.get("privateMB").and_then(|v| v.as_u64()).or(Some(800)),+ s.get("privateMB").and_then(|v| v.as_u64()).or(Some(800)).map(|m| m + growth), s.get("handles").and_then(|v| v.as_u64()).map(|h| h as u32).or(Some(3000)), ) }
src/memory.rs+91@@ -381,6 +381,44 @@ pub fn log_suffix() -> String { } } +// ── hidden documents (0.10.6) ───────────────────────────────────────────────++/// What the hidden-creation bracket did.+#[derive(Debug)]+pub struct Bracket<T, E> {+ pub made: Result<T, E>,+ /// DocumentVisible(false) succeeded (if not, nothing was created).+ pub set_ok: bool,+ /// DocumentVisible(true) succeeded afterwards.+ pub restored: bool,+}++/// THE HIDDEN-CREATION BRACKET, shared by the COM host and the simulator:+/// `set_visible(false)`, then `make` (only if that worked), then+/// `set_visible(true)` — ALWAYS, whatever `make` did, retried up to `tries`+/// times. ISldWorks::DocumentVisible is a SESSION setting: left false, every+/// document of that type the user opens next would be invisible.+pub fn hidden_bracket<T, E>(+ mut set_visible: impl FnMut(bool) -> Result<(), E>,+ make: impl FnOnce() -> Result<T, E>,+ tries: usize,+) -> Bracket<T, E> {+ let off = set_visible(false);+ let set_ok = off.is_ok();+ let made = match off {+ Ok(()) => make(),+ Err(e) => Err(e),+ };+ let mut restored = false;+ for _ in 0..tries.max(1) {+ if set_visible(true).is_ok() {+ restored = true;+ break;+ }+ }+ Bracket { made, set_ok, restored }+}+ // ── the guard ─────────────────────────────────────────────────────────────── /// Verbs that create or load geometry.@@ -723,6 +761,59 @@ mod tests { assert!(matches!(between_checks(Pressure::Unknown, Some(10), b, None, None), Between::Continue), "nothing to act on"); } + #[test]+ fn the_hidden_bracket_always_puts_the_setting_back() {+ use std::cell::RefCell;+ let calls = RefCell::new(Vec::new());+ // Creation fails: the setting is still restored.+ let b = hidden_bracket(+ |v| {+ calls.borrow_mut().push(v);+ Ok::<(), String>(())+ },+ || Err::<u32, String>("NewDocument failed".into()),+ 3,+ );+ assert!(b.made.is_err() && b.set_ok && b.restored);+ assert_eq!(*calls.borrow(), vec![false, true], "off, then on — after a failure too");+ // The restore fails twice, then works: retried, and reported restored.+ let n = RefCell::new(0);+ let b = hidden_bracket(+ |v| {+ if v {+ *n.borrow_mut() += 1;+ if *n.borrow() < 3 {+ return Err("busy".to_string());+ }+ }+ Ok(())+ },+ || Ok::<u32, String>(7),+ 3,+ );+ assert_eq!((b.made, b.restored, *n.borrow()), (Ok(7), true, 3));+ // It never works: reported NOT restored, after every try.+ let n = RefCell::new(0);+ let b = hidden_bracket(|v| if v { *n.borrow_mut() += 1; Err("no".to_string()) } else { Ok(()) }, || Ok::<u32, String>(1), 3);+ assert!(!b.restored && *n.borrow() == 3);+ // DocumentVisible(false) fails: nothing is created — and it is still set back.+ let made = RefCell::new(false);+ let calls = RefCell::new(Vec::new());+ let b = hidden_bracket(+ |v| {+ calls.borrow_mut().push(v);+ if v { Ok(()) } else { Err("refused".to_string()) }+ },+ || {+ *made.borrow_mut() = true;+ Ok::<u32, String>(1)+ },+ 3,+ );+ assert!(!b.set_ok && b.made.is_err() && !*made.borrow() && b.restored);+ assert_eq!(*calls.borrow(), vec![false, true]);+ }+ #[test] fn the_report_and_its_hint_name_the_holders() { let s = Sample {
src/script.rs+33−8@@ -171,10 +171,16 @@ pub enum AppearanceTarget { #[derive(Debug, Clone, PartialEq)] pub enum Op {- /// Create a new part document from the resolved part template.- NewPart,+ /// Create a new part document from the resolved part template. `visible`+ /// false creates it HIDDEN (ISldWorks::DocumentVisible bracketed around+ /// NewDocument, 0.10.6): no window, no graphics context.+ NewPart { visible: bool }, /// Create a new assembly document from the resolved assembly template.- NewAssembly,+ NewAssembly { visible: bool },+ /// DELETE EVERY FEATURE after the origin of a part the BRIDGE created+ /// (0.10.6): N geometry checks cost one document instead of N. Refused on+ /// any document the bridge did not create.+ ClearPart, /// ONE sketch holding every entity listed (0.10.0) — several closed /// profiles in one sketch extrude into rings, walls or hole patterns in ONE /// feature. `shorthand` is the op the caller wrote: "sketch", "sketch_rect"@@ -206,8 +212,9 @@ impl Op { /// The slug used in the DSL and echoed in the per-step result. pub fn name(&self) -> &'static str { match self {- Op::NewPart => "new_part",- Op::NewAssembly => "new_assembly",+ Op::NewPart { .. } => "new_part",+ Op::NewAssembly { .. } => "new_assembly",+ Op::ClearPart => "clear_part", Op::Sketch { shorthand, .. } => shorthand, Op::Extrude(_) => "extrude", Op::Cut(_) => "cut",@@ -239,6 +246,7 @@ impl Op { pub const OPS: &[&str] = &[ "new_part", "new_assembly",+ "clear_part", "sketch", "sketch_rect", "sketch_circle",@@ -673,6 +681,16 @@ fn count(v: &Value, key: &str) -> Result<u32, String> { Ok(n as u32) } +/// `visible` on new_part / new_assembly: a boolean, default true (a user's+/// script shows its part).+fn visible_arg(step: &Value) -> Result<bool, String> {+ match step.get("visible") {+ None | Some(Value::Null) => Ok(true),+ Some(Value::Bool(b)) => Ok(*b),+ Some(other) => Err(format!("visible must be true or false, got {other}")),+ }+}+ fn feature_ref(step: &Value) -> String { step.get("feature").and_then(|v| v.as_str()).filter(|s| !s.trim().is_empty()).unwrap_or("last").to_string() }@@ -683,8 +701,9 @@ fn parse_step(step: &Value) -> Result<Op, String> { .and_then(|v| v.as_str()) .ok_or_else(|| format!("missing `op` (one of: {})", OPS.join(", ")))?; Ok(match op {- "new_part" => Op::NewPart,- "new_assembly" => Op::NewAssembly,+ "new_part" => Op::NewPart { visible: visible_arg(step)? },+ "new_assembly" => Op::NewAssembly { visible: visible_arg(step)? },+ "clear_part" => Op::ClearPart, "sketch_rect" => Op::Sketch { shorthand: "sketch_rect", plane: plane_ref(step)?, entities: vec![rect(step)?] }, "sketch_circle" => Op::Sketch { shorthand: "sketch_circle", plane: plane_ref(step)?, entities: vec![circle(step)?] }, "sketch" => {@@ -811,11 +830,17 @@ pub fn validate(ops: &[Op], has_open_doc: bool) -> Result<(), String> { for (i, op) in ops.iter().enumerate() { let at = i + 1; match op {- Op::NewPart | Op::NewAssembly => {+ Op::NewPart { .. } | Op::NewAssembly { .. } => { doc = true; have_feature = false; continue; }+ Op::ClearPart if doc => {+ // Every feature is gone: nothing to call "last", no sketch pending.+ have_feature = false;+ sketch_pending = false;+ continue;+ } _ if !doc => { return Err(format!( "step {at} ({}) needs a document: start the script with new_part, or open one \
src/selftest.rs+101−7@@ -179,23 +179,47 @@ 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.+ let mut shared: Option<String> = None; for check in battery() { if !only.is_empty() && !only.iter().any(|o| o == check.id) { continue; }- if let Err(stop) = memory_between(ctx, last_check, check.id, memory_override.as_deref(), &mut memory_log) {+ if let Err(stop) = memory_between(ctx, last_check, check.id, memory_override.as_deref(), &mut memory_log, &mut shared) { memory_log.stopped = Some(stop); break; }- geometry_check(ctx, &mut run, &check, memory_override.as_deref());+ let reuse = reusable(&check);+ if reuse && shared.is_none() {+ shared = make_shared_document(ctx, memory_override.as_deref());+ if shared.is_some() {+ memory_log.documents_created += 1;+ }+ }+ let target = if reuse { shared.clone() } else { None };+ if target.is_none() {+ memory_log.documents_created += 1;+ }+ let clear_failed = geometry_check(ctx, &mut run, &check, memory_override.as_deref(), target.as_deref()); let after = crate::memory::sample(crate::launchos::os().as_ref()); if let Some(last) = run.results.last_mut() { last["swPrivateMbAfter"] = json!(after.sw_private_mb);+ last["reusedDocument"] = json!(target.is_some());+ }+ if clear_failed {+ // A shared part that could not be cleared is replaced, not reused.+ close_shared(ctx, &mut shared); } last_check = Some(check.id); }+ close_shared(ctx, &mut shared); if memory_log.stopped.is_none() {- if let Err(stop) = memory_between(ctx, last_check, "the verbs on a scratch part", memory_override.as_deref(), &mut memory_log) {+ if let Err(stop) = memory_between(ctx, last_check, "the verbs on a scratch part", memory_override.as_deref(), &mut memory_log, &mut shared) { memory_log.stopped = Some(stop); } }@@ -207,7 +231,8 @@ 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"});+ let mut new_doc = json!({"type": "part", "visible": false});+ memory_log.documents_created += 1; if let Some(r) = &memory_override { new_doc["ignoreMemoryPressure"] = json!(true); new_doc["reason"] = json!(r);@@ -268,6 +293,8 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value { /// What the memory checks between the geometry checks did. #[derive(Default)] struct MemoryLog {+ /// Documents the run created (the shared part counts once).+ documents_created: usize, recycled: Vec<Value>, /// Checks run on under `ignoreMemoryPressure` where the run would have stopped. overridden: Vec<Value>,@@ -278,6 +305,7 @@ impl MemoryLog { fn to_json(&self, over: Option<&str>) -> Value { let mut v = json!({ "recycled": self.recycled,+ "documentsCreated": self.documents_created, "complete": self.stopped.is_none(), "memory": crate::memory::report(crate::launchos::os().as_ref()), });@@ -294,7 +322,14 @@ impl MemoryLog { /// Before one geometry check: read memory, and recycle or stop if it calls /// for it. `Err` stops the run, with everything that explains why.-fn memory_between(ctx: &Ctx, last: Option<&str>, next: &str, over: Option<&str>, log: &mut MemoryLog) -> Result<(), Value> {+fn memory_between(+ ctx: &Ctx,+ last: Option<&str>,+ next: &str,+ over: Option<&str>,+ log: &mut MemoryLog,+ shared: &mut Option<String>,+) -> Result<(), Value> { use crate::memory::Between; let os = crate::launchos::os(); let s = crate::memory::sample(os.as_ref());@@ -302,6 +337,9 @@ fn memory_between(ctx: &Ctx, last: Option<&str>, next: &str, over: Option<&str>, if !crate::memory::needs_attention(p, s.sw_private_mb, s.budget_mb) { return Ok(()); }+ // The run's own shared part is closed first: it is the bridge's, and a+ // session can only be recycled with nothing open.+ close_shared(ctx, shared); let facts = crate::memory::session_facts(ctx, os.as_ref()); match crate::memory::between_checks(p, s.sw_private_mb, s.budget_mb, facts.bridge_started, facts.open_docs) { Between::Continue => Ok(()),@@ -326,6 +364,48 @@ fn memory_between(ctx: &Ctx, last: Option<&str>, next: &str, over: Option<&str>, } } +/// 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.+fn reusable(c: &Check) -> bool {+ let Some(steps) = c.script.as_array() else { return false };+ if steps.first().and_then(|s| s.get("op")).and_then(|o| o.as_str()) != Some("new_part") || c.expect.features.is_some() {+ return false;+ }+ steps.iter().skip(1).all(|st| {+ let op = st.get("op").and_then(|o| o.as_str()).unwrap_or("");+ if matches!(op, "set_material" | "set_appearance" | "set_prop" | "new_part" | "new_assembly" | "save_as") {+ return false;+ }+ let named = |v: Option<&Value>| v.and_then(|f| f.as_str()).map(|f| f != "last").unwrap_or(false);+ !(named(st.get("feature")) || named(st.pointer("/edges/allOfFeature")))+ })+}++/// The battery's shared part: hidden, the bridge's own.+fn make_shared_document(ctx: &Ctx, over: Option<&str>) -> Option<String> {+ let mut a = json!({"type": "part", "visible": false});+ if let Some(r) = over {+ a["ignoreMemoryPressure"] = json!(true);+ a["reason"] = json!(r);+ }+ let made = crate::verbs::dispatch(ctx, "swx_new_document", &a);+ made.pointer("/documentIdentity/path")+ .or_else(|| made.pointer("/created/path"))+ .and_then(|p| p.as_str())+ .filter(|p| !p.is_empty())+ .or_else(|| made.pointer("/created/title").and_then(|t| t.as_str()))+ .map(String::from)+}++fn close_shared(ctx: &Ctx, shared: &mut Option<String>) {+ if let Some(t) = shared.take() {+ let key = if t.contains('\\') || t.contains('/') { "path" } else { "targetDocument" };+ let _ = crate::verbs::dispatch(ctx, "swx_close_doc", &json!({key: t, "save": false}));+ }+}+ fn compact(v: &Value) -> Value { json!({ "success": v.get("success"),@@ -703,10 +783,23 @@ fn titles(ctx: &Ctx) -> Vec<String> { } /// One check: build, MEASURE, compare, close what it made.-fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<&str>) {+fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<&str>, reuse: Option<&str>) -> bool { let t = Instant::now(); let before = titles(ctx);- let mut sargs = json!({"script": c.script});+ 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})+ }+ }; if let Some(r) = memory_override { sargs["ignoreMemoryPressure"] = json!(true); sargs["reason"] = json!(r);@@ -877,4 +970,5 @@ fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<& "measured": measured, "fails": fails, }));+ reuse.is_some() && r.get("error").and_then(|e| e.as_str()).map(|e| e.contains("(clear_part) failed")).unwrap_or(false) }
src/verbs.rs+65−1@@ -1316,6 +1316,65 @@ fn stop_refusal_hint(code: &str) -> &'static str { /// rung, success measured by a PROCESS CENSUS rather than a COM call, and nothing /// disowned until the process is verifiably gone. Never a kill. Never answers a /// dialog. See `crate::quit` for the three laws and the refcount reasoning.+/// `swx_com_respawn` (0.10.6) — THE REFERENCE QUESTION, SETTLED BY MEASURING.+///+/// Measured 2026-10-14: each scratch document created and closed left+/// SOLIDWORKS ~83 MB and ~125 handles heavier, with GetDocumentCount back at+/// 0. If the BRIDGE pins something (a COM reference its host never released),+/// respawning the host releases it and the handle count drops; if not, nothing+/// changes. This respawns ONLY the bridge's own com host child — never+/// SOLIDWORKS, which it does not call at all — and refuses while a verb holds+/// the host. Ledgered.+fn swx_com_respawn(ctx: &Ctx, args: &Value) -> Value {+ let Some(reason) = arg_str(args, "reason").map(str::trim).filter(|r| r.chars().count() >= 8) else {+ return err_code(+ "reason_required",+ "swx_com_respawn needs reason:\"why\" (8+ characters)",+ "It restarts the bridge's own COM host process; the reason goes into the ledger beside the \+ old and new host pids. SOLIDWORKS is not touched.",+ );+ };+ let snap = ctx.com.snapshot();+ if snap.busy {+ return err_code(+ "com_host_busy",+ format!(+ "a verb is in flight on the com host ({}); swx_com_respawn never pulls the host out from under a COM call",+ snap.in_flight.unwrap_or_else(|| "unknown".into())+ ),+ "Wait for it to finish (swx_progress shows it), then call again. Nothing was changed.",+ );+ }+ match ctx.com.respawn() {+ Err(why) => err_code(+ "com_host_busy",+ format!("the com host could not be respawned: {why}"),+ "Nothing was changed. swx_progress shows what is in flight.",+ ),+ Ok(rep) => {+ crate::ledger::record(+ "com_host_respawn",+ json!({"by": "swx_com_respawn", "reason": reason, "oldHostPid": rep.old_pid,+ "newHostPid": rep.new_pid, "gracefulExit": rep.graceful_exit}),+ );+ ok(json!({+ "respawned": true,+ "oldHostPid": rep.old_pid,+ "newHostPid": rep.new_pid,+ "gracefulExit": rep.graceful_exit,+ "solidworksTouched": false,+ "windowEtiquette": "background",+ "_hint": "The bridge's COM host was replaced. The old one left through its own loop, so its \+ backend was dropped and every COM reference it held was released by Release() — \+ not left to COM's ~6-minute ping rundown (gracefulExit:false means it had to be \+ killed, and then the rundown applies). SOLIDWORKS was not called: the new host \+ attaches on the next verb. Compare SOLIDWORKS's handle count (swx_status.memory) \+ before and after: a drop means the bridge was pinning something.",+ }))+ }+ }+}+ fn swx_stop(ctx: &Ctx, args: &Value) -> Value { gate!(ctx); use crate::quit;@@ -1943,7 +2002,9 @@ fn prime_session( Ok(_) => return Err("a dialog is up".into()), Err(e) => return Err(format!("the window census could not be read: {e}")), }- let made = crate::verbs3::new_document(ctx, &json!({"type": "part"}));+ // 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})); 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))); }@@ -3060,6 +3121,7 @@ pub const CATALOG: &[(&str, &str, &str)] = &[ ("swx_sign_in", "exec", "sign in to 3DEXPERIENCE for a launch this bridge started (or, with userAuthorized:true, any 3DEXPERIENCE ID login window, e.g. Visualize's or the platform updater's, recognized by its content; window:visualize|solidworks|desktopUpdate|<hwnd> picks one, and a named hwnd is probed by content, never refused on census grounds), from the user's local vault (the password never leaves the laptop or reaches a reply); the page is prepared first (user name, Continue), and ab's policy (sign_in_vault_policy, measured) currently keeps the password for the user to type; fails closed and never retries a rejected password"), ("swx_user_presence", "read", "is the user working in this SOLIDWORKS? foreground + last input + documents the bridge did not open (the guard every quit and sweep passes)"), ("swx_kill", "process", "force-kill SOLIDWORKS (reason + confirm; discards unsaved work)"),+ ("swx_com_respawn", "process", "respawn ONLY the bridge's own COM host child (reason): every COM reference the bridge holds is released; SOLIDWORKS is never called; refused while a verb is in flight"), ("swx_window_info", "read", "SOLIDWORKS' windows and its main hwnd (read-only, nothing raised)"), ("swx_dismiss_blocking_dialogs", "exec", "census of blocking SOLIDWORKS dialogs; close:true is opt-in, gated, and never touches an unattributed window"), ("swx_close_window", "exec", "close one window by hwnd (reason + confirm)"),@@ -3209,6 +3271,7 @@ pub const STARTUP_EXEMPT: &[(&str, &str)] = &[ ("swx_sign_in", "acts on the LAUNCHER's sign-in window, never through COM — it is how a start gets past the sign-in at all"), ("swx_user_presence", "a read about the USER, needed most during a start; asks COM for documents only on a verified-ready session"), ("swx_selftest", "never reaches the com host itself: every check it runs goes through this same gate, and a check held at it is REPORTED — a self-test that refused to run while SOLIDWORKS starts could not say so"),+ ("swx_com_respawn", "respawns the bridge's own com host child and never calls SOLIDWORKS — the readiness gate would"), ]; /// swx_stop is gated with one carve-out: under `userAuthorized`, a plan that@@ -3400,6 +3463,7 @@ fn dispatch_after_gate(ctx: &Ctx, command: &str, args: &Value) -> Value { "swx_progress" => swx_progress, "swx_start" => swx_start, "swx_stop" => swx_stop,+ "swx_com_respawn" => swx_com_respawn, // DREW'S STANDING AUTHORIZATION, 2026-09-11: the bridge closes documents // and quits by itself, having first written every document to disk. "swx_close_all_safely" => crate::verbs3::close_all_safely,
src/verbs3.rs+163−11@@ -3778,11 +3778,65 @@ fn sanitise_stem(stem: &str) -> String { if trimmed.is_empty() { "doc".into() } else { trimmed } } +/// AFTER A HIDDEN CREATION: ISldWorks::DocumentVisible is a SESSION setting.+/// The host puts it back itself (three tries); if it reports it could not, the+/// server asks once more, and either way the outcome is ledgered — left false,+/// every document of that type the USER opens next would be invisible.+pub(crate) fn settle_document_visible(ctx: &Ctx, kind: &str, visibility: &Value) -> Value {+ let mut v = visibility.clone();+ if visibility.get("requested").and_then(|r| r.as_bool()) != Some(false) {+ return v;+ }+ let restored = visibility.get("documentVisibleRestored").and_then(|r| r.as_bool()) == Some(true);+ if restored {+ crate::ledger::record("document_visible_toggled", json!({"type": kind, "restored": true}));+ return v;+ }+ let retry = ctx.call("restore_document_visible", json!({"type": kind}), 30);+ let ok = retry.is_ok();+ v["serverRetry"] = json!({"restored": ok, "error": retry.err().map(|e| e.message)});+ v["documentVisibleRestored"] = json!(ok);+ crate::ledger::record(+ if ok { "document_visible_restored_on_retry" } else { "document_visible_restore_failed" },+ json!({"type": kind, "why": "ISldWorks::DocumentVisible is a session setting; left false, the user's next documents of this type open invisible"}),+ );+ if !ok {+ v["warning"] = json!(format!(+ "SOLIDWORKS's DocumentVisible setting for {kind} documents could NOT be put back: documents of that \+ type may open INVISIBLE in this session until SOLIDWORKS restarts. Tell the user."+ ));+ }+ v+}+ pub fn new_document(ctx: &Ctx, args: &Value) -> Value { gate!(ctx); let kind = arg_str(args, "type").unwrap_or("part").to_string();+ // HIDDEN DOCUMENTS (0.10.6): `visible:false` creates it with no window+ // (ISldWorks::DocumentVisible around NewDocument). The default stays+ // visible — a user's document is one they want to see.+ let visible = match args.get("visible") {+ None | Some(Value::Null) => true,+ Some(Value::Bool(b)) => *b,+ Some(other) => {+ return err_code("bad_argument", format!("visible must be true or false, got {other}"), "Omit it for a visible document.")+ }+ }; let job = ctx.jobs.start("new_document", format!("Creating a new {kind}"), 15);- let call_args = json!({"type": kind, "template": args.get("template").cloned()});+ let mut call_args = json!({"type": kind, "template": args.get("template").cloned(), "visible": visible});+ // A HIDDEN document gets its scratch file from the host, through the very+ // document object it created: it is not necessarily the ACTIVE document, so+ // "save the active one" (give_identity) could name somebody else's.+ if !visible && !arg_bool(args, "keepUnsaved", false) {+ let ext = match kind.as_str() {+ "assembly" => "SLDASM",+ "drawing" => "SLDDRW",+ _ => "SLDPRT",+ };+ let n = crate::scratch::next_scratch_seq();+ let path = crate::scratch::scratch_docs_dir().join(format!("hidden-{}-{n}.{ext}", sanitise_stem(&kind)));+ call_args["scratchPath"] = json!(path.to_string_lossy());+ } let started = std::time::Instant::now(); match ctx.call("new_document", call_args, 110) { // A FAILED new_document IS THE EXHAUSTION SIGNAL. On 2026-09-03 both the@@ -3791,12 +3845,22 @@ pub fn new_document(ctx: &Ctx, args: &Value) -> Value { // an ordinary template problem — which sent the caller looking for a // fault in a template that was fine. Look at what the machine is // actually holding before blaming the template.- Err(e) => match exhausted_error(ctx, &e) {- Some(v) => v,- None => sidecar_error(e),- },+ Err(e) => {+ // A HIDDEN CREATION THAT FAILED still flipped a session setting: it+ // must be back on before anything else happens.+ let settled = e.data.as_ref().and_then(|d| d.get("visibility")).map(|v| settle_document_visible(ctx, &kind, v));+ let mut out = match exhausted_error(ctx, &e) {+ Some(v) => v,+ None => sidecar_error(e),+ };+ if let (Some(v), Value::Object(m)) = (settled, &mut out) {+ m.insert("visibility".into(), v);+ }+ out+ } Ok(r) => { crate::ledger::record_time("swx_new_document", started.elapsed().as_secs_f64());+ let settled = r.get("visibility").map(|v| settle_document_visible(ctx, &kind, v)); let mut identity = json!({"identity": "unknown"}); if let Some(t) = r.get("created").and_then(|c| c.get("title")).and_then(|t| t.as_str()) { let t = t.to_string();@@ -3835,9 +3899,13 @@ pub fn new_document(ctx: &Ctx, args: &Value) -> Value { "title": r.get("created").and_then(|c| c.get("title")).cloned(), "type": kind, "template": r.get("template").cloned(),+ "visible": visible, }), ); let mut r = r;+ if let (Some(v), Some(o)) = (settled, r.as_object_mut()) {+ o.insert("visibility".into(), v);+ } if let Some(o) = r.as_object_mut() { if let Some(c) = o.get_mut("created").and_then(|v| v.as_object_mut()) { if let Some(t) = identity.get("title").and_then(|v| v.as_str()) {@@ -3855,7 +3923,17 @@ pub fn new_document(ctx: &Ctx, args: &Value) -> Value { "documentIdentity": identity, "windowEtiquette": "background", "progress": job.finished(),- "_hint": if titled_only {+ "_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."+ } 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 \ second document of this title appears, neither can be addressed by any verb, \@@ -4531,12 +4609,35 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value { }; // Parse + validate HERE, before anything touches the live session: a script // that cannot work must never half-run on someone's CAD application.- let ops = match crate::script::parse(&script) {+ let mut ops = match crate::script::parse(&script) { Ok(o) => o, Err(e) => return script_help(e), };+ let mut script = script;+ // REUSE ONE DOCUMENT FOR A BATTERY (0.10.6). Measured 2026-10-14: every+ // created-and-closed document cost SOLIDWORKS ~83 MB and ~125 handles for+ // good. `reuseDocument` runs the script in a part the BRIDGE created, its+ // leading new_part becoming clear_part — N checks, ONE document.+ let reuse = arg_str(args, "reuseDocument").map(String::from);+ if let Some(r) = &reuse {+ if ops.iter().any(|o| matches!(o, crate::script::Op::NewAssembly { .. })) {+ return script_help("reuseDocument reuses a PART: a script that makes an assembly cannot run in it");+ }+ let news = ops.iter().filter(|o| matches!(o, crate::script::Op::NewPart { .. })).count();+ let leads = matches!(ops.first(), Some(crate::script::Op::NewPart { .. }));+ if news > 1 || (news == 1 && !leads) {+ return script_help("with reuseDocument, new_part may only be the FIRST step (it becomes clear_part)");+ }+ if leads {+ ops[0] = crate::script::Op::ClearPart;+ if let Some(first) = script.as_array_mut().and_then(|a| a.first_mut()) {+ *first = json!({"op": "clear_part"});+ }+ }+ let _ = r;+ } let makes_its_own_doc =- matches!(ops.first(), Some(crate::script::Op::NewPart | crate::script::Op::NewAssembly));+ matches!(ops.first(), Some(crate::script::Op::NewPart { .. } | crate::script::Op::NewAssembly { .. })); let has_doc = ctx .call("docs", json!({}), 30) .ok()@@ -4562,10 +4663,37 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value { let mut target_owned: Option<bool> = None; let mut pinned_path: Option<String> = None; if !makes_its_own_doc {- let target = match resolve_document(ctx, args, "swx_run_script") {+ let target_args = match &reuse {+ Some(r) if r.contains('\\') || r.contains('/') => json!({"path": r}),+ Some(r) => json!({"targetDocument": r}),+ None => args.clone(),+ };+ let target = match resolve_document(ctx, &target_args, "swx_run_script") { Ok(t) => t, Err(e) => return e, };+ if reuse.is_some() && !target.owned {+ return err_code(+ "reuse_not_owned",+ format!("reuseDocument names '{}', which this bridge did not create", target.title),+ "reuseDocument clears and rebuilds the part, so it only ever names a part the bridge made \+ (swx_new_document {\"type\":\"part\",\"visible\":false} makes one). Nothing was changed.",+ );+ }+ // CLEAR_PART DELETES EVERY FEATURE: only in a part the bridge created,+ // whatever write grant the call carries (0.10.6).+ let clears_before_making = ops+ .iter()+ .position(|o| matches!(o, crate::script::Op::ClearPart))+ .map(|i| !ops[..i].iter().any(|o| matches!(o, crate::script::Op::NewPart { .. })))+ .unwrap_or(false);+ if clears_before_making && !target.owned {+ return err_code(+ "clear_part_not_owned",+ format!("clear_part deletes every feature, and '{}' is not a part this bridge created", target.title),+ "clear_part only ever runs in the bridge's own scratch parts. Nothing was changed.",+ );+ } target_owned = Some(target.owned); pinned_path = target.path.clone(); grant = match crate::writes::authorize_in(ctx, args, "swx_run_script", crate::writes::Kind::ModifyDocument, Some(&target), None) {@@ -4591,6 +4719,8 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value { let call_args = crate::writes::with_grant(json!({"script": script, "targetDocument": pinned, "targetPath": pinned_path}), &grant); match ctx.call("run_script", call_args, 280) { Err(e) => {+ // A HIDDEN new_part that failed still flipped a session setting.+ let settled = e.data.as_ref().and_then(|d| d.get("visibility")).map(|v| settle_document_visible(ctx, "part", v)); // A SCRIPT THAT FAILED AFTER ITS new_part STILL MADE THAT PART: the // bridge owns it, so swx_close_doc / swx_reset_workspace can close it // (0.10.2 — a failed self-test check left one open, un-closable).@@ -4603,6 +4733,9 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value { if let (Value::Object(m), Some(c)) = (&mut out, created) { m.insert("createdBeforeFailing".into(), c); }+ if let (Value::Object(m), Some(v)) = (&mut out, settled) {+ m.insert("visibility".into(), v);+ } out } Ok(mut r) => {@@ -4611,6 +4744,16 @@ pub fn run_script(ctx: &Ctx, args: &Value) -> Value { // was measured live on THIS SOLIDWORKS release — one table, // compat::OP_FACTS (docs/LIVE-RESULTS.md). attach_proofs(&mut r, &ops);+ // HIDDEN new_part steps (0.10.6): the session setting, settled and+ // ledgered per step.+ if let Some(steps) = r.get_mut("steps").and_then(|s| s.as_array_mut()) {+ for st in steps.iter_mut() {+ let kind = if st.get("op").and_then(|o| o.as_str()) == Some("new_assembly") { "assembly" } else { "part" };+ if let Some(v) = st.pointer("/result/visibility").cloned() {+ st["result"]["visibility"] = settle_document_visible(ctx, kind, &v);+ }+ }+ } if let Some(t) = r .get("created") .and_then(|c| c.get("created").and_then(|x| x.get("title")).or_else(|| c.get("title")))@@ -4691,6 +4834,14 @@ fn script_help(message: impl Into<String>) -> Value { ); if let Value::Object(map) = &mut v { map.insert("ops".into(), json!(crate::script::OPS));+ map.insert(+ "verbArgs".into(),+ json!({+ "script": "the steps (required)",+ "targetDocument / path": "the document a script with no new_part runs in",+ "reuseDocument": "the title or path of a part the BRIDGE created: the script runs in it and its leading new_part becomes clear_part — a battery of N checks costs ONE document (each created-and-closed document cost SOLIDWORKS ~83 MB, measured 2026-10-14)",+ }),+ ); map.insert( "example".into(), json!({@@ -4708,8 +4859,9 @@ fn script_help(message: impl Into<String>) -> Value { map.insert( "opArgs".into(), json!({- "new_part": {},- "new_assembly": {},+ "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_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"}, "sketch_rect": {"plane": "as sketch", "face": "as sketch", "widthMm": "required", "heightMm": "required", "xMm": 0, "yMm": 0, "anchor": "corner (default: xMm/yMm is the CORNER) | center", "centerMm": "[x, y] (implies center)"}, "sketch_circle": {"plane": "as sketch", "face": "as sketch", "radiusMm": "required (or diameterMm)", "xMm": 0, "yMm": 0, "centerMm": "[x, y]"},
tests/e2e.rs+11@@ -859,6 +859,17 @@ fn every_verb_over_http_against_the_real_binary() { ); check!(b, "verb_times gives p50 and p90", r["stages"]["swx_run_script"]["p90"].is_number(), s(&r)); + // ── the reference diagnostic (0.10.6) ───────────────────────────────────+ let docs_before = b.call("swx_docs", json!({}))["docs"].as_array().map(|a| a.len());+ let r = b.call("swx_com_respawn", json!({"reason": "release every COM reference the bridge holds"}));+ check!(+ b,+ "swx_com_respawn replaces the bridge's own com host and leaves SOLIDWORKS's documents alone",+ r["respawned"] == true && r["solidworksTouched"] == false+ && b.call("swx_docs", json!({}))["docs"].as_array().map(|a| a.len()) == docs_before,+ s(&r)+ );+ // ── OWNERSHIP: the bridge must not change documents it did not create ─── // The suite has a user-owned document open (gearbox.sldasm, opened by this // test, never created by the bridge). Every mutating verb must refuse it.
tests/readiness.rs+1−1@@ -375,7 +375,7 @@ const EXEMPT: &[&str] = &[ "swx_action_ledger", "swx_verb_times", "swx_uninstall", "swx_find_files", "swx_recover_list", "swx_window_info", "swx_dismiss_blocking_dialogs", "swx_close_window", "swx_screenshot", "swx_set_driving_badge", "swx_kill", "swx_user_presence", "swx_sign_in",- "swx_selftest",+ "swx_selftest", "swx_com_respawn", ]; #[test]
tests/visibility.rsadded+374@@ -0,0 +1,374 @@+//! 0.10.6 — FIXING THE MEMORY GROWTH AT ITS ROOT.+//!+//! MEASURED 2026-10-14 (~21:00Z, hosted 0.10.5, SOLIDWORKS 2026 fresh start,+//! 12 x [new_part + a 20x20x10 box] then swx_reset_workspace): SOLIDWORKS's+//! private bytes went 520 MB -> 913 -> 1191 -> then +83 MB per part to 2035 MB;+//! 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.++mod common;++use common::{Bridge, TempDir};+use serde_json::{json, Value};++fn s(v: &Value) -> String {+ serde_json::to_string_pretty(v).unwrap_or_default()+}++fn boot(tag: &str, extra: &[(&str, &str)]) -> (Bridge, TempDir) {+ let machine = TempDir::new(tag);+ std::fs::write(machine.join("windows.json"), "[]").unwrap();+ let mut env: Vec<(&str, &str)> = vec![("SWX_LAUNCH_FAKE_DIR", Box::leak(machine.path().into_boxed_str()))];+ env.extend_from_slice(extra);+ let b = Bridge::start_with(&env);+ (b, machine)+}++/// Every ISldWorks::DocumentVisible call the simulator saw, in order.+fn visible_calls(b: &Bridge) -> Vec<Value> {+ std::fs::read_to_string(b.scratch.join("document-visible.jsonl"))+ .unwrap_or_default()+ .lines()+ .filter_map(|l| serde_json::from_str(l).ok())+ .collect()+}++fn ledger(b: &Bridge) -> Vec<Value> {+ b.call("swx_action_ledger", json!({"limit": 500}))["actions"].as_array().cloned().unwrap_or_default()+}++fn doc_count(b: &Bridge) -> usize {+ b.call("swx_docs", json!({}))["docs"].as_array().map(|a| a.len()).unwrap_or(usize::MAX)+}++fn box_script(first: Value) -> Value {+ json!([first,+ {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},+ {"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", &[]);+ b.call("swx_start", json!({}));+ std::fs::write(b.scratch.join("mock-newdoc-fails"), "").unwrap();+ let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));+ let calls = visible_calls(&b);+ check!(+ b,+ "NewDocument failed inside the bracket: an error — and DocumentVisible was put back anyway",+ r["success"] == false && r["visibility"]["documentVisibleRestored"] == true+ && calls.len() == 2 && calls[0]["visible"] == false && calls[1]["visible"] == true,+ format!("{}\n{}", s(&r), s(&json!(calls)))+ );+ std::fs::remove_file(b.scratch.join("mock-newdoc-fails")).unwrap();+ let v = b.call("swx_new_document", json!({"type": "part"}));+ let shown = b.call("swx_show_part", json!({"path": v["documentIdentity"]["path"]}));+ check!(+ b,+ "...so the next ordinary document is VISIBLE",+ v["success"] == true && shown["visibility"]["wasVisible"] == true,+ format!("{}\n{}", s(&v), s(&shown))+ );+}++#[test]+fn a_setting_the_host_could_not_put_back_is_retried_and_otherwise_shouted() {+ let (b, _m) = boot("vis-retry", &[]);+ b.call("swx_start", json!({}));+ // The host's three restores fail; the server's second chance works.+ std::fs::write(b.scratch.join("mock-docvisible-restore-fails"), "").unwrap();+ let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));+ check!(+ b,+ "the server retried the restore, it worked, and the retry is ledgered",+ r["success"] == true && r["visibility"]["serverRetry"]["restored"] == true+ && r["visibility"]["documentVisibleRestored"] == true+ && ledger(&b).iter().any(|e| e["action"] == "document_visible_restored_on_retry"),+ s(&r)+ );+ std::fs::write(b.scratch.join("mock-docvisible-restore-fails-always"), "").unwrap();+ let r = b.call("swx_new_document", json!({"type": "part", "visible": false}));+ check!(+ b,+ "a restore that cannot be done is a WARNING to tell the user, and ledgered",+ r["visibility"]["documentVisibleRestored"] == false+ && r["visibility"]["warning"].as_str().unwrap_or("").contains("INVISIBLE")+ && ledger(&b).iter().any(|e| e["action"] == "document_visible_restore_failed"),+ s(&r)+ );+}++#[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")]);+ b.call("swx_start", json!({}));+ b.call("swx_new_document", json!({"type": "part"}));+ let before = b.call("swx_status", json!({}));+ let docs = doc_count(&b);+ let r = b.call("swx_com_respawn", json!({}));+ check!(b, "swx_com_respawn needs a reason", r["errorCode"] == "reason_required", s(&r));+ std::fs::write(b.scratch.join("mock-log-commands"), "").unwrap();+ let r = b.call("swx_com_respawn", json!({"reason": "measure handles before and after"}));+ let reached = std::fs::read_to_string(b.scratch.join("mock-commands.log")).unwrap_or_default();+ check!(+ b,+ "swx_com_respawn: a new host, the old one left gracefully, and NOTHING reached SOLIDWORKS",+ r["success"] == true && r["respawned"] == true && r["solidworksTouched"] == false && r["gracefulExit"] == true+ && r["oldHostPid"] == before["com"]["hostPid"] && r["newHostPid"] != r["oldHostPid"]+ && reached.trim().is_empty(),+ format!("{}\nreached SOLIDWORKS: {reached:?}", s(&r))+ );+ let after = b.call("swx_status", json!({}));+ check!(+ b,+ "...the same SOLIDWORKS session with the same documents, now served by the new host; ledgered",+ after["com"]["hostPid"] == r["newHostPid"] && after["com"]["processId"] == before["com"]["processId"]+ && doc_count(&b) == docs+ && ledger(&b).iter().any(|e| e["action"] == "com_host_respawn" && e["detail"]["reason"] == "measure handles before and after"),+ format!("{}\n{}", s(&before["com"]), s(&after["com"]))+ );+ // MID-VERB: a slow verb holds the host; the respawn refuses.+ let refused = std::thread::scope(|sc| {+ let slow = sc.spawn(|| b.call("swx_start", json!({})));+ std::thread::sleep(std::time::Duration::from_millis(700));+ let r = b.call("swx_com_respawn", json!({"reason": "while a verb is running"}));+ let _ = slow.join();+ r+ });+ check!(+ b,+ "swx_com_respawn while a verb holds the host: refused com_host_busy, naming the verb, nothing changed",+ refused["errorCode"] == "com_host_busy" && refused["error"].as_str().unwrap_or("").contains("(start)"),+ s(&refused)+ );+}++#[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+/// closes its own shared part FIRST, so its bridge-started session can be+/// recycled (an open document would stop the run instead).+#[test]+fn reuse_keeps_solidworks_flat_and_the_shared_part_is_closed_before_a_recycle() {+ let machine = TempDir::new("vis-growth");+ std::fs::write(machine.join("windows.json"), "[]").unwrap();+ std::fs::write(machine.join("quit-script.json"), json!({"a": "exited"}).to_string()).unwrap();+ std::fs::write(machine.join("sldworks-created"), "1700000000").unwrap();+ std::fs::write(+ machine.join("sldworks.json"),+ json!({"responding": true, "hwnd": 6622700, "privateMB": 520, "growthPerDocumentMB": 83, "handles": 2845}).to_string(),+ )+ .unwrap();+ std::fs::write(+ machine.join("memory.json"),+ json!({"commitLimitMB": 78438, "commitAvailMB": 40000, "physTotalMB": 32153, "physAvailMB": 9000, "processes": []}).to_string(),+ )+ .unwrap();+ let scratch = TempDir::new("vis-growth-scratch");+ std::fs::write(scratch.join("mock-solidworks-session"), "700900").unwrap();+ std::fs::write(+ scratch.join("launch-state.json"),+ json!({"attempt": {"id": 1, "startedAt": 1700000000u64, "shortcut": "x.lnk", "route": "shellExecute",+ "invokedAt": 1700000000u64, "stage": "attached", "finished": true, "outcome": "attached",+ "runBeginSeen": true, "attributedSolidworks": {"pid": 700900, "createdAt": 1700000000u64}},+ "preferredRoute": "shellExecute", "consecutiveFailures": 0})+ .to_string(),+ )+ .unwrap();+ let b = Bridge::start_in(+ scratch,+ &[+ ("SWX_LAUNCH_FAKE_DIR", Box::leak(machine.path().into_boxed_str())),+ ("SWX_QUIT_SETTLE_MS", "80"),+ ("SWX_QUIT_EXIT_WAIT_SECS", "3"),+ ("SWX_RECYCLE_POLL_MS", "100"),+ ("SWX_RECYCLE_READY_SECS", "30"),+ // 600 MB: the shared part (one document, 603 MB) already passes it.+ ("SWX_SW_MEMORY_BUDGET_MB", "600"),+ ],+ );+ b.call("swx_start", json!({}));+ let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03", "T05"]}));+ 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);+ let rec = r["recycled"].as_array().cloned().unwrap_or_default();+ check!(+ b,+ "after T01 built the shared part (603 MB > 600), the run CLOSED its own part and recycled before T03 — it did not stop",+ r["complete"] == true && rec.len() == 1 && rec[0]["beforeCheck"] == "T03" && rec[0]["solidworksPrivateMbBefore"] == 603,+ s(&r)+ );+ check!(+ b,+ "every check passed, T01 in the first shared part and T03/T05 in a new one",+ r["summary"]["failed"] == 0 && row("T01")["swPrivateMbAfter"] == 603+ && row("T03")["reusedDocument"] == true && row("T05")["reusedDocument"] == true,+ s(&json!(rows))+ );+ check!(b, "nothing was terminated", !machine.join("terminated.jsonl").exists(), "terminated.jsonl");+}
tests/wiring.rs+1−1@@ -1069,7 +1069,7 @@ fn the_startup_exemption_list_is_pinned_name_by_name() { "swx_action_ledger", "swx_verb_times", "swx_uninstall", "swx_find_files", "swx_recover_list", "swx_window_info", "swx_dismiss_blocking_dialogs", "swx_close_window", "swx_screenshot", "swx_set_driving_badge", "swx_kill", "swx_user_presence", "swx_sign_in",- "swx_selftest",+ "swx_selftest", "swx_com_respawn", ]; pinned.sort(); assert_eq!(
uninstall.sh