main
Download ZIP Propose a change
Name Last commit message Last updated
docs 0.11.4 source · Drew Owens 1d ago
examples 0.10.10 source · Drew Owens 5d ago
manifests canary: 0.12.0-rc1 · Drew Owens 21h ago
references 0.11.4 source · Drew Owens 1d ago
resources 0.11.4 source · Drew Owens 1d ago
screenshots hero: the open-source Moveo robot arm inside the app on a dark viewport, generated in Hero Studio and approved by Drew;… · Drew Owens 18d ago
skills 0.10.9 source · Drew Owens 6d ago
src 0.11.4 source · Drew Owens 1d ago
tests 0.11.4 source · Drew Owens 1d ago
tools 0.11.4 source · Drew Owens 1d ago
.gitignore 0.11.0 source · Drew Owens 5d ago
adom-bridge-solidworks-manifest.json manifest: 0.11.4 as published · Drew Owens 1d ago
BRIDGE_VERSION 0.11.4 source · Drew Owens 1d ago
bridge.json 0.11.4 source · Drew Owens 1d ago
Cargo.lock 0.11.4 source · Drew Owens 1d ago
Cargo.toml 0.11.4 source · Drew Owens 1d ago
ECOSYSTEM-TABLE.md v0.3.1: manifest, README, skill, ecosystem table, parity report · Drew Owens 1mo ago
FUSION-PARITY-REPORT.md v0.3.1: manifest, README, skill, ecosystem table, parity report · Drew Owens 1mo ago
install.sh Publish 0.11.4 · Drew Owens 1d ago
LICENSE 0.3.3 source + crate build: BOM exported from the SOLIDWORKS assembly, build log §1–11, release script · Drew Owens 1mo ago
package.json 0.11.4 source · Drew Owens 1d ago
page.json page.json: 0.11.4 (hero, title, brief, tags) · Drew Owens 1d ago
README.md 0.11.4 source · Drew Owens 1d ago
SKILL.md 0.11.4 source · Drew Owens 1d ago
uninstall.sh Publish 0.11.4 · Drew Owens 1d ago

SOLIDWORKS bridge (swx)

Say swx to the AI to reach this bridge. It drives the SOLIDWORKS desktop app on Windows over its COM API: streamlined file management, BOM generation from assemblies, and first-line design analysis — all from any Adom container, with SOLIDWORKS staying quietly in the background.

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 — 71 verbs

Area Verbs
Create & open swx_new_document (part/assembly/drawing from the stock template), swx_open, swx_import (STEP/IGES/Parasolid/STL), swx_save, swx_save_as, swx_close_doc, swx_find_files, swx_docs
Model swx_run_script — a typed modelling DSL: new_part, sketch (rectangles, circles, lines, polylines, arcs — centre/angles, three-point, tangent — slots, polygons, sketch fillets and chamfers, construction geometry; several closed profiles in ONE sketch, contours checked closed before anything is drawn), sketch_rect, sketch_circle (on a named plane, an offset plane, a face, or a plane offset from a face), extrude / cut (reverse, both directions, mid-plane, through all, up to next, up to a face, offset from a face, draft), revolve / revolve_cut, mirror (features or bodies), hole (plain, counterbore, countersink), fillet, chamfer, shell, linear_pattern, circular_pattern, set_appearance, set_material (with a density), set_prop, save_as, rebuild, zoom_fit. Everything in millimetres; a worked LEGO brick is in examples/lego-2x4.json, and CSWA exam parts with their published answers in resources/cswa/. Measured live on SOLIDWORKS 2026 (30 of 30 checks) and 2021 (a community self-test): docs/LIVE-RESULTS.md; the 0.11.0 ops are not yet measured live.
Macros swx_run_macro — ISldWorks::RunMacro2 on a .swp/.swb/.dll. Arbitrary code, so it needs the write opt-in every time. Scaffolding: not yet run live.
Assemble swx_add_component — insert a part or sub-assembly into the active assembly, positioned and rotated.
Close & quit safely swx_close_all_safely (saves every open document to a recovery folder, verifies each file, then closes), swx_shutdown (that sweep, then a graceful quit), swx_recover_list (what was preserved and where), swx_close_import_temps, swx_user_presence (is someone working in this session? the bridge refuses to end a session a person is using).
Export swx_export plus per-format aliases swx_export_step/iges/stl/3mf/glb/sat/parasolid/pdf/dxf/dwg, swx_pack_and_go
Render swx_render_image (MEASURED live) — a high-resolution PNG of the active document from SOLIDWORKS's own viewport (RealView, ambient occlusion, shadows, perspective, a plain background), at a size independent of the window; every display setting it changes is restored.
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_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)

Verb names and arg names deliberately mirror the Adom Fusion 360 bridge (outputPath, filePath, name, parameters, bodies, current, applied, stages, wouldPrompt) so an AI that already knows fusion_* can drive swx_* with nothing new to learn. docs/FUSION-PARITY-REPORT.md covers all 132 Fusion verbs with a verdict, and is where the divergences are argued.

Two things to know before you drive it

SOLIDWORKS Connected is launched through the Platform's own shortcut. On the 3DEXPERIENCE editions (including Design Professional for Makers), CoCreateInstance spawns a process that refuses with "SOLIDWORKS Design must be launched from the 3DEXPERIENCE Platform" and never attaches — verified live, and for two rounds this bridge concluded from that the edition could not be started at all. That was a conclusion about the wrong experiment. The Start-Menu shortcut the Platform installs does not run sldworks.exe; it runs

CATSTART.exe -run "SWXDesktopLauncher.exe"
             -object "--AppName=\"SWXCSWK_AP\" -tenant=<TENANT> -monoapp
                      -3DRegistryURL=https://<region>-makers-registry.3dexperience.3ds.com"
             -nowindow

so the refusal was ours, for bypassing the launcher chain. swx_start now invokes that shortcut — the sanctioned artefact, carrying the user's own tenant, with nothing reverse-engineered and no argument invented — then polls for the COM server. A cold start runs 1-3 minutes, so it answers started:true, stillLaunching:true while the application comes up rather than holding the request. Classic desktop installs still attach directly over COM. errorCode:"host_app_launch_blocked" now means only that no shortcut could be found (SWX_SOLIDWORKS_SHORTCUT points the bridge at one).

The launcher stalls, so the launch is a state machine (v0.4.0, full write-up in docs/LAUNCH-RESILIENCE.md). Measured on 2026-09-04: five of five launches through cmd /c start produced a launcher log that stopped at SecurityContext: and never logged Run Begin — the chain sat idle at ~50 MB forever — while every interactive launch of the SAME shortcut with the SAME arguments succeeded. So swx_start now invokes ONE route, watches the launcher's own log, and on a stall ends only the chain it started itself before trying the next route (explorer → ShellExecuteExW → Start-Process → cmd /c start), remembering the one that works. It also tracks the attempt on disk, so calling swx_start during a launch reports it instead of starting a second one — the promise the old hint made and the old code did not keep.

The kill allowlist is two images, and the INVOKE — not the census — decides what is ours. The bridge may terminate CATSTART.exe and SWXDesktopLauncher.exe, and only the instances descending from a root the invocation itself handed back (ShellExecuteExW's process handle, or Start-Process -PassThru's pid), with image AND creation time re-checked on the open handle at the moment of the kill. Routes that cannot say what they started (explorer, cmdStart) never clean up at all. It must never kill 3DEXPERIENCELauncher* — those are the Platform's own daemons, they are not required for a launch (the successful one had no Backbone running), and a real user launch rebuilds them. sldworks.exe is never killed here either: ending the user's CAD session is swx_kill, which is gated behind their explicit opt-in. There is no image-name kill and no tree kill anywhere in the launch path.

Exports are copies; save-as is a rename. swx_export* uses SaveAs3 in Silent|Copy mode, so the open document keeps its identity (v0.2 did not, and silently renamed it). swx_save_as is the deliberate rename; copy:true makes it a copy. And because SaveAs3 picks the format from the DESTINATION EXTENSION rather than the format argument, that is where the format rules are enforced — swx_export_step {"outputPath":"x.wrl"} is refused.

Writes are DENY-BY-DEFAULT. These verbs pass through one choke point (src/writes.rs), which refuses unless both: the document is one this bridge created in this SOLIDWORKS session, and the destination path does not already exist —

swx_save · swx_save_as · swx_export and its ten format aliases · swx_bom {format:"csv"} · swx_pack_and_go · swx_close_doc · swx_props_set · swx_set_parameter · swx_set_preference · swx_run_script · swx_rebuild_check (it calls ForceRebuild3) · swx_stop and swx_close_all_documents (which end the user's session, and are gated unconditionally) · swx_run_macro (always gated: a macro is a VBA project with the whole API in scope, and nothing about the call predicts what it will touch).

The gate opens BOTH ways. When the user has opted in, in full, the bridge does the thing — it does not refuse anyway. The server and the com host check the identical chain independently, so a partial chain is refused twice and a complete one is honoured by both.

Anything refused returns errorCode:"user_writes_disabled", "destination_exists" or "not_bridge_document". Exporting from any document to a path that does not exist always works — that is the normal way to get data out, and it needs no permission.

The only way past it is a deliberate opt-in that a caller cannot grant itself: the bridge process must be started with SWX_ALLOW_USER_WRITES=1 and the call must carry allowUserWrites:true, confirm:true and a reason. Every granted exception is recorded in swx_action_ledger. swx_status and swx_describe report userWrites: "disabled"|"enabled" so a caller knows the posture before it plans anything.

The same rule covers ending or disturbing the user's session: swx_stop, swx_kill, swx_close_all_documents, and closing ANY dialog — swx_dismiss_blocking_dialogs {close:true} and swx_close_window alike, including dialogs the bridge itself raised. swx_close_window additionally refuses an application frame outright (closing one quits SOLIDWORKS, which is swx_stop's job) and refuses a dialog that asks about the user's own work (Save changes?, Document Recovery, licence) at any posture. It also covers swx_open {readOnly:false}, which takes a write lock on the user's file and converts-and-dirties an older-version document in memory.

For everything that reaches SOLIDWORKS, the com host enforces the same two rules independently, on its dispatch loop rather than per command: it keeps its own record of the documents it created (stamped with the SOLIDWORKS session, so a crash or a restart invalidates it), reads the opt-in from its own environment, and refuses any command not on its no-side-effects list. A command added tomorrow is therefore deny-by-default.

Where there is only one veto, and why: swx_kill, swx_close_window, swx_dismiss_blocking_dialogs and swx_screenshot go out through ab rather than the com host, so the host's veto is not in their path — the server-side choke point is the only check they have. That is worth knowing when reading the rest of this section. swx_screenshot captures BY HANDLE (the one COM reports, or an hwnd swx_window_info attributes to SOLIDWORKS); it will not take a picture of an arbitrary window on the desktop.

This posture is a response to five consecutive rounds of adversarial review, in each of which the bridge turned out to be able to write to the user's data through a route whose guard had been forgotten. Gating routes failed five times; denying by default at one choke point removes the class.

Architecture

ab (Adom Bridge) ── HTTP /command ──> swx-bridge.exe            (server mode)
                                         │  JSON-lines over stdio
                                         └─> swx-bridge.exe --com-host
                                             (child process holding the live
                                              SldWorks.Application COM object)
  • One exe, two modes. The server spawns the com host from its own current_exe(), so there is exactly one file to ship and one image name for ab to stop (killImageName: "swx-bridge.exe" covers both processes).
  • The COM object lives in a CHILD PROCESS, not a thread. A wedged COM call — SOLIDWORKS parked on a modal dialog, a rebuild that never returns — cannot be killed as a thread. It can be killed as a process. The server owns that lifecycle: lazy spawn, one request in flight, per-request timeout, kill and respawn after a crash or hang, and the child's stderr tail surfaced in the failing verb's detail. A child that dies between requests is replaced transparently, so the next verb works.
  • The child is persistent: it attaches to (or launches) SOLIDWORKS once and keeps the COM object, so verbs don't pay a per-call attach. It is initialized as an STA and holds the apartment for its whole life. On Windows it is spawned with CREATE_NO_WINDOW — no console ever blips on the user's desktop — and it exits on its own when its parent goes away (stdin EOF), so nothing is left holding a COM object.
  • Late-bound IDispatch (GetIDsOfNames + Invoke), like every SOLIDWORKS macro: it works against whichever version is installed, with no interop assemblies to match.
  • Mock mode (SWX_MOCK=1, automatic on non-Windows) swaps the COM backend for an in-memory fake behind the same Backend trait — same protocol, same response shapes — so the whole verb surface is testable with no SOLIDWORKS and no Windows.

Host app status

SOLIDWORKS is a detect-only host app (ab tier-2): Dassault publishes no silent installer — desktop trial serials come from a reseller, and the Makers edition comes from the 3DEXPERIENCE Platform store — so the bridge reports absence with errorCode:"host_app_not_installed" and a next-steps _hint instead of ever attempting an install. The verbs that need no COM (swx_status, swx_readiness, swx_describe, swx_progress, swx_find_files, swx_action_ledger, swx_verb_times, swx_uninstall) work regardless, so "what CAD files do I have?" is answerable today.

swx_status.memory reports the machine's commit charge, SOLIDWORKS's own committed memory and the five largest holders. These are read-only facts: the bridge never acts on another application. While committed memory is critical, verbs that create or load geometry are refused with low_commit_memory. See the solidworks-memory skill.

Rendering

swx_render_image {reason, width:3840, height:2160, view:"isometric", realView:true, ambientOcclusion:true, shadows:true, perspective:true, zoomToFit:true, zoomFactor:1.5} draws the active document with SOLIDWORKS's own viewport (PhotoView 360 is absent on this edition) through IModelDoc2::SaveBMP at the size asked, converts it to PNG, and reads the size back from the file — capped:true if only the window size came out. MEASURED 2026-10-05 on the 110-document Moveo assembly with the window minimized: 3840×2160 in ~11 s (1080p in 10.4 s), shaded with a floor shadow and ambient occlusion, every setting read back restored. RealView did not take on this laptop (uncertified GPU): every set is read back, a set SOLIDWORKS ignored is why:"setIgnored" (never "applied") and the reply's notes says so. zoomFactor >1 zooms in; after a fit 1.4–1.6 fills the frame (2.0 clipped the tall Moveo). Every display setting it changes is captured first and restored after, on errors too; settings accounts for each one the caller asked for ({requested, before, after, changed, restored, why}); the document is never saved. A render that changes the screen waits for the user to be away. Still unmeasured: a visible window, 8192 px, background changes. SOLIDWORKS Visualize can be started (swx_start {app:"visualize"}, through its platform shortcut, its dialogs reported and never answered); a Visualize render is design-only. Details: docs/RENDERING.md.

Export formats, verified rather than assumed

Every format below was exported from a real solid on a live SOLIDWORKS Design Professional for Makers 2026 and the bytes on disk were checked — with the caveat that those probes used SaveAs3 with Silent ALONE, which is how the rename bug was found. The shipped export path uses Silent|Copy and has never run; whether Copy is valid for PDF/PNG/JPG/TIF is unknown. Read the table as "SOLIDWORKS can write this format", not "the bridge's export path is proven":

Works from a part Refused for a part (code 256) Blocked by this bridge
STEP, STP, IGES, STL, X_T, X_B, SAT, 3MF, GLB, PLY, AMF, 3DXML, PNG, JPG, TIF, XAML, PDF OBJ, DXF, DWG, U3D (DXF/DWG are drawing formats) VRML (.wrl)

VRML is refused outright with errorCode:"format_blocked": it raises a modal option sheet that no unattended caller can answer, and on the live install it hung the COM call until the watchdog killed the host.

Development

cargo build                                      # native (mock mode: no SOLIDWORKS needed)
cargo test                                       # unit + HTTP e2e + protocol e2e
cargo build --release --target x86_64-pc-windows-gnu   # the shipped swx-bridge.exe

# run it by hand and poke it
SWX_MOCK=1 cargo run -- --port 24695
curl -s localhost:24695/status | jq
curl -s localhost:24695/command -d '{"command":"swx_describe","args":{}}' | jq

The suite is built so it cannot lie:

  • No test asserts by reading source. Every check drives the real compiled binary (CARGO_BIN_EXE_swx-bridge) over HTTP or over the stdio protocol.
  • A verb-coverage gate compares the verbs actually CALLED during the suite against bridge.json, in both directions, and against swx_describe. A new verb shipped without a test turns the suite red.
  • Response invariants on every call: 200, parses as JSON, boolean success; malformed bodies get a clean 400, never a 500.
  • Effects, not existence: exports are read back off disk, the BOM CSV is parsed and its columns checked, find_files runs against real fixture files.
  • Protocol e2e: the real server driving the real com-host child, including crash recovery (kill the child, the next verb still works), junk on the child's stdout, orphan cleanup, and a slow verb that must not block swx_progress.

Everything the bridge writes goes under one data root, %LOCALAPPDATA%\adom-solidworks-bridge\ (or SWX_SCRATCH_DIR for tests): the recovery folders, the action ledger, the ownership registry, launch state, verb timings, settings and the bridge's own exports. It is a directory only this bridge writes to, and it is outside everything ab owns:

  • not ab's install cache (Adom Bridge\bridges-cache\solidworks\, where the bridge itself is installed): ab's updater re-extracts it, and until 0.9.0 the data lived there — the 0.8.9 auto-install of 2026-09-18 deleted every recovery folder from the day before (measured 2026-09-30);
  • not ab's own directory (Adom Bridge\): its installer and uninstaller own it, and what an ab uninstall, repair or reinstall does to anything else in it is undocumented. 0.9.0 kept its data in Adom Bridge\solidworks-data\; 0.9.1 moved it out (adversary pass 39).

Both older locations are migrated on start — copied under a temporary name, verified byte for byte, renamed into place, never over an existing file, the originals left where they were, each file recorded by size, mtime and sha256, and copied again beside the first copy if it changes later (see migrated-from-legacy.json). With neither LOCALAPPDATA nor USERPROFILE the root would fall back to the OS temp directory, which Windows empties on a schedule, so every sweep then refuses (data_root_is_temp); and a sweep refuses outright (recovery_inside_install_dir) if its recovery folder would land under any install location. Exports are pruned at 48h / 300 files. swx_uninstall removes the bridge's own bookkeeping and exports, and never the recovery folders or the ledger — a recovery file can be the only copy of work the user had not saved.

swx_com_eval {"expr":"$sw.RevisionNumber()","reason":"..."} is the maintainer escape hatch: a late-bound call chain rooted at $sw or $doc, for proving out a new capability against a real install before it becomes a verb.

Live validation

Mock-green is not green: v0.2 passed 49 mock tests while shipping four COM bugs, all of which the first live session found (see the parity report). Three tools close that gap:

python3 tools/swx-live.py <verb> '<json>' [timeoutSec]   # one verb on the desktop
python3 tools/swx-batch.py calls.json                    # many verbs in ONE round trip
python3 tools/live-smoke.py                              # the full live suite

Every release is also checked against CSWA exam parts (0.11.0): resources/cswa/ holds exam-style parts with their PUBLISHED answers (the official Dassault sample exam's Tool Block, steps 1-4; the 3dengr practice set; the LEGO brick as spec 0) and community parts whose keys are derived analytically (each spec's derivation reproduces its key in Python). python3 tools/cswa-check.py builds each one live, weighs it with swx_mass_properties, and compares it with the key within the exam's 1 %; swx_selftest {"level":"cswa"} does the same on any install. See tools/cswa/README.md.

The release ritual: every publish is proven live (0.11.4), on the very bytes that ship.

Only a PUBLISHED build reaches the test machine: ab installs what a manifest names, after checking the zip's sha256, and reaps anything side-loaded. So the build is first published as a release CANDIDATE that no one else gets (the canary). The live check runs on that candidate, and the publish then ships those same bytes.

  1. Commit the release (version bump, tests, docs).

  2. tools/release.sh --canary builds the zip and publishes it as the release <version>-rc<N>. It also writes the page file manifests/canary.json, a manifest naming that candidate, and commits it: the record holds the code commit and the sha256 of the exe inside the zip. It reads the page's copy back, then prints the next three commands. The main manifest, which every other machine follows, is not touched.

  3. On the test machine only, install the candidate: adom-bridge --target adom_drew2 --ai-thread "SolidWorks Bridge" bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/solidworks-bridge/files/manifests/canary.json"}'. This does not pin the machine to the canary. ab polls a bridge's own declared updateManifestUrl first (adom-bridge help bridge_check_updates). Every zip's bridge.json declares the MAIN manifest there, and the canary file says the same.

  4. Run python3 tools/cswa-check.py --target adom_drew2 --report auto there. The report lands in tools/cswa/reports/, and it records which build ran in bridge:

    • the installed exe's sha256, read back through ab's read_file;
    • the manifest it was installed from, and that zip's sha256.
  5. Commit the report, then run tools/release.sh --require-live tools/cswa/reports/<that report> --publish. This PROMOTES the canary without rebuilding. It refuses unless:

    • the canary is of this version;
    • no code was committed since it was built;
    • the zip in dist/ (or the candidate's asset, downloaded) has the canary's sha256;
    • the report ran the exe inside that zip.

    It then prints how to put the test machine back on the main manifest, should it still name the canary.

--publish on its own is REFUSED before anything is built. tools/check-live-report.py accepts a report only when it comes from cswa-check and was made by THIS bridge version. It must also have run EVERY part shipped in resources/cswa/ (a run narrowed with --only proves nothing about the rest; a part may be skipped only for an op the bridge lacks), passed every part it ran, and not stopped. It must be newer than the last commit that touched src/, resources/ or Cargo.toml: a report made before the last code change proves nothing about this build. Finally, it must say, in bridge.exeSha256, that it ran the exe being published.

The one exception is tools/release.sh --docs-only --publish, for a release that changes no code. tools/check-docs-only.py checks the changes since the last publish (the last commit to the release manifest). It refuses anything in src/ or resources/, untracked files included, since resources/ ships as it is on disk. It refuses any change to Cargo.toml or Cargo.lock beyond the package's own version line. A docs-only release's changelog starts with "Docs-only release: no code changed since … the live CSWA gate was skipped".

live-smoke.py creates its own scratch part, builds a real 50×30×20 mm solid, reads the geometry back, drives a dimension, exports eight formats, checks the bytes on disk, and then asserts SOLIDWORKS is left with exactly the documents it started with. It never opens, saves or closes a document it did not create, and it never launches or stops SOLIDWORKS.

Closing and quitting

Drew, 2026-09-11: "I despise every time you ask for help closing. You MUST find/build a way to safely close the docs/program."

So swx_shutdown {reason} does the whole thing: it saves every open document into a recovery folder, closes them, and quits SOLIDWORKS. It needs no write opt-in and no per-document confirmation, because the authorization is standing.

The authorization is to CLOSE. It is not an authorization to lose anything, and those are different permissions. What replaces the question is a sequence:

nothing closes before it is on disk.

Every document, the user's or the bridge's, saved or not, is written to %LOCALAPPDATA%\adom-solidworks-bridge\recovery\<yyyymmdd-hhmmss>\ and verified on disk before anything closes. A document that could not be written is *not* closed — closing a document whose only copy is in memory is the one outcome this design exists to prevent, and the response names any such document in remaining. A document the user had saved and then edited is copied into recovery under a new name, so their file on disk is never modified and their unsaved changes survive too. Because every document is clean by the time a close happens, a save prompt cannot appear; if any dialog does appear, the sweep stops and reports it rather than answering it.

"On disk" is checked structurally, not by existence and not by size alone. A full disk leaves a zero-byte file behind and exists() says yes to it; it also leaves truncated ones, which any byte floor admits. So each recovery file is verified to be a SOLIDWORKS document — the container marker at the head of the file — and then to be at least 16 KB. The reply publishes both, recoverySignatureOk and recoveryBytes, per document. The check fails closed: anything unreadable or unrecognised leaves the document open.

What the container marker is was measured, not looked up. 166 real .SLDPRT/.SLDASM/.SLDDRW files from three hardware repositories: none begins with the OLE compound-document signature every reference names for SOLIDWORKS, and all 166 carry 00 00 00 04 at offset 4. Modern SOLIDWORKS is not a compound file. Both are accepted — an older document from an archive really is one — and the real headers are pinned in the suite, because a check on a format this bridge does not own is only as good as its samples.

Before any of that, the sweep asks whether it fits. Marker and floor catch an empty file and a foreign one; neither catches the right file, cut short — the marker sits at offset 4, so every prefix of a real document carries it, and a truncation past 16 KB clears the floor. The format offers nothing cheap that would catch it. So the sweep estimates what the session will write — on-disk sizes for documents it can measure, and for the rest a floor that depends on the document's KIND, because an assembly is about thirty times the median part — and refuses to begin when the recovery volume has less than twice that free: insufficient_headroom, naming free bytes, needed bytes and the shortfall, with nothing written and nothing closed. It asks again before every save, because the sweep is itself one of the things filling the volume. headroomBytesBefore and estimatedWriteBytes are reported on every run, including successful ones.

The dialog census runs before every save and before every close, not once per batch. A save can raise a prompt of its own (a missing external reference, a rebuild question), and under a per-batch check a dialog raised by the first save stayed invisible until every other document had already been closed. batch is now only a bound on round trips and wall time.

The dialog check has a third answer besides yes and no: unknown. If the window census cannot be read, this bridge cannot say whether a prompt is on screen, and the two halves of the sweep treat that differently — saving loses nothing, so the saves go ahead; closing while blind can lose everything, so nothing closes. The response is stoppedBy:"dialogCensusUnknown" with dialogCensusReadable:false, the work is already safe in the recovery folder, and running it again when the census is readable finishes the job.

The saves also use SaveAs3 with swSaveAsOptions_Silent alone — no Copy bit — which re-points the open document at the recovery file. That is deliberate: the close that follows is then closing a document whose entire content is already on disk, and the user's original file is left exactly as it was. Both halves are pinned by tests that read the original bytes back and read the close key out of an ordering journal.

Verb What it does
swx_shutdown {reason} preserve everything, close everything, quit. Idempotent.
swx_close_all_safely {reason} the same sweep, without the quit
swx_recover_list what each sweep preserved, and where — newest first

swx_stop is unchanged and still quits only a session with zero documents open; under userAuthorized:true it no longer requires a launch attribution (the user may quit a session this bridge did not start) and the window-message rungs become available, which matters when COM is wedged behind a licence dialog. It still never kills anything.

Install, launch, sign-in

This install is SOLIDWORKS Connected / Makers (3DEXPERIENCE), and the first fact about it governs everything else: COM cannot launch it. swx_status reports canLaunchProgrammatically:false. COM can attach to a session that is already running, and that is all it can do — a property of the edition, not a limitation of the bridge, so no argument or retry changes it.

What can start it is the shortcut the 3DEXPERIENCE Platform installed. swx_start invokes that .lnk, which starts CATSTART.exe, which starts SWXDesktopLauncher.exe, which starts sldworks.exe. The launcher chain exits once sldworks.exe is up, so by the time SOLIDWORKS is usable there is no launcher process left — nothing to notify on the way out, and nothing to reap.

Attribution, measured on Drew's machine 2026-09-11. The shellExecute route invokes through ShellExecuteExW with SEE_MASK_NOCLOSEPROCESS, and the live run got a real process handle back — CATSTART pid 131272, creation time 1789148101 — and that chain reached the launcher's Run Begin. That handle is the point. The bridge's law is no handle, no kill: it may only clean up a chain the invoke itself handed back. On this machine that world is confirmed — the route that launches is also the route that can be cleaned up.

The 3DEXPERIENCE ID sign-in

A cold launch can stop at a 3DEXPERIENCE ID sign-in window. It is a native WebView2 window — window class DSYSysLoginPanelWebView2Class, title "Login | 3DEXPERIENCE ID | Dassault Systemes", owned by SWXDesktopLauncher.exe in the bridge's own chain. UIA sees no controls inside it, so no automation can read or fill it. That includes this bridge.

As of 0.6.0 the bridge recognises that window, reports stage helperSignIn, pauses every launch deadline while it is on screen (a human deciding to type is not a stalled launcher, and killing the chain under them would be wrong), never kills or escalates the chain, and sends the user a toast saying sign-in is needed. You type the password; the bridge never asks for, stores or transmits one. Tick Remember me the first time so later launches do not stop here. How long this stage lasts depends on when someone answers the window, not on any timer the bridge owns.

The same sign-in page has two more hosts, both measured. SOLIDWORKS Visualize shows it in a WinForms window titled "Login - 3DEXPERIENCE ID - Dassault Systemes". The platform updater SWXDesktopUpdate.exe (started from %TEMP% the moment a platform shortcut is invoked) shows it in a window titled just "3DEXPERIENCE", and every app start waits behind that one. Since 0.9.23 the bridge recognises a login by its CONTENT (an "Email or username" or "Password" field), whatever its title. swx_status and swx_start then report waitingOn:"userSignIn" with signInWindow {hwnd, title, ownerImage, host}.

The launch stages

swx_status.launch.stage — and swx_start's own launch block — reports exactly one of these:

stage What to do
invoked The route was just invoked. Poll swx_status.
launcherStarted The chain is running but has not reached Run Begin. This is the stage that stalls; the bridge cleans up and tries the next route itself. Do not invoke the shortcut any other way.
runBegin The chain is really building. Keep polling; sldworks.exe appears within seconds.
helperSignIn The sign-in window is up. Only you can clear it — sign in on the desktop.
sldworksUp The application exists; the COM server is not answering yet. Keep polling.
attaching Polling for the COM server. Keep polling; do not launch again.
attached COM is usable. The only success.
stalled That route stalled and its chain was cleaned up. The bridge is trying the next route. Nothing to retry by hand.
exhausted Every route was tried and none attached. Start SOLIDWORKS from the Start Menu or the Platform; an interactive launch works when this one does not.

Three of those are the milestones that matter operationally: runBegin (the chain is really building), sldworksUp (the application exists) and attached (COM is usable — the only success). stalled and exhausted are the two terminal failures.

stillLaunching:true means a launch this bridge started is still in flight. Calling swx_start again does not start a second chain; it attaches to the one already running. That is deliberate: four launcher chains once ended up on a user's desktop because a caller retried.

Quitting, and why it is close to a one-way door

swx_stop quits only a session with zero documents open, and force does not waive that — force waives the unsaved question only. If documents are open, close them, or drain leaked import conversion temporaries with swx_close_import_temps first.

The reason the count is not negotiable is on the other side of the quit: on this edition, relaunching takes 1-3 minutes and can stop at the sign-in window again, which only a person at the keyboard can answer. Quitting is cheap; getting back is not.

Roadmap

  • v0.11.4 (this): every publish is proven live; Offset Entities; a spec for every op nothing had built.

    Recorded first: the first live CSWA run (2026-10-15, bridge 0.11.2, SOLIDWORKS 2026). It passed 8 of 8, every part within 0.0015 % of its key (docs/LIVE-RESULTS.md). Every variant those scripts used is now measured live (LIVE_2026_10_15_CSWA). Six were built for the first time: lines and arcs, the face-offset plane, the plain hole, the density, and a sketch on a +Z face.

    Now:

    • The live gate is the rule. tools/release.sh --publish refuses without --require-live <report>, before anything is built. --docs-only skips the gate, and its changelog says so. It is allowed only when tools/check-docs-only.py finds nothing changed in src/ or resources/ (untracked files included) and nothing in Cargo.toml or Cargo.lock beyond the version. tools/check-live-report.py now also needs EVERY shipped part to have run: a run narrowed with --only is refused. The ritual is under "Live validation" above.

    • The canary is what ships. Only a published build reaches the test machine. So tools/release.sh --canary publishes the build as the release candidate <version>-rc<N>, with manifests/canary.json naming it; it is installed by hand, on the test machine only. --require-live <report> --publish then promotes those exact bytes, with no rebuild. It does so only when the report says it ran that exe (bridge.exeSha256, which cswa-check reads back from the installed copy) and no code changed since the canary. cswa-check --report auto names the report itself.

    • offsetEntities: a model face's outline offset into the sketch by SOLIDWORKS (IModelDoc2::SketchOffsetEntities2, available since 2001Plus, so on 2021 too). It takes the outer loop's edges or the whole face. Its sign does not fix the side, so the bridge reads where the new segments landed, and redoes the offset the other way once.

    • regionsAtMm on extrude / cut: SOLIDWORKS' Selected Contours, done as its own example does it (EnableContourSelection, then SelectByID2 "SKETCHREGION" at mark 4).

    • Tool Block Step 4 runs, two ways. cswa-08 uses Offset Entities, regions and two offsetFromSurface cuts; it runs live only. cswa-08b draws the same pocket from the explicit inset profile, using only ops already measured. Both predict 434.72 g, +0.49 % of the key.

    • Community specs cswa-09 to cswa-12 build every op no spec built:

      • revolve and revolve-cut (360°, and 180° mid-plane);
      • mirror (features, bodies, an offset plane);
      • slots, a hexagon, and counterbored and countersunk holes;
      • upToNext, upToSurface and offsetFromSurface;
      • draft on a boss and on a cut.

      Each key is derived analytically. Each spec's derivation re-derives its key in Python (tools/cswa-check.py --derivations), and the spec holds SOLIDWORKS to 0.1 %.

    • The first canary caught two bugs, which is what it is for. 0.11.4-rc1 ran all 14 parts live on 2026-10-16 (tools/cswa/reports/cswa-2026-10-16-sw2026-0.11.4-rc1-FAILED.json): cswa-00 to cswa-10 passed within 0.1 %, cswa-11 and cswa-12 did not, and the mass and centre of mass they measured say exactly why (docs/LIVE-RESULTS.md):

      • The draft flag was inverted. FeatureExtrusion3 / FeatureCut4 take Ddir1; the 2024 help says true = inward, and SOLIDWORKS 2026 did the opposite: the 45 deg countersink cut widened into the outward frustum (804.25 mm3 more than the inward one, to the decimal) and the 5 deg boss widened. The bridge now passes true for outward (modeling::draft_outward_flag).
      • "Up to surface" is end condition 4, not 10. With swEndCondUpToSelection 10 and the face at mark 1, a boss and a cut both built with an empty direction box and stopped at the next face (3392.9 and 1005.3 mm3 short). swEndCondUpToSurface 4, the id the macro recorder writes, is what the bridge sends now; offsetFromSurface 5 with the same selection had gone to the face.
      • Every extrude and cut now reads back what SOLIDWORKS stored (readBack: the end condition, depth and draft side from IExtrudeFeatureData2), so a request the feature did not honour shows in the reply.
      • 0.11.4-rc2 then passed 14 of 14 on 2026-10-20 (tools/cswa/reports/cswa-2026-10-20-sw2026-0.11.4.json), and those are the bytes this release ships.
      • The ten variants the passing parts exercised are measured (LIVE_2026_10_16_RC1: revolve and revolve-cut, mirror about planes, construction geometry, Offset Entities, sketch regions, the offset cut); the five the failing parts exercised stay unmeasured and say CAUGHT until 0.11.4-rc2 passes.
    • A derivedAppend may no longer shadow a name its base derives. In the simulator, Step 4's tpx had redrawn Step 1's R5 fillet.

    • The simulator offsets a plain block's face. It models the regions of profiles that nest or stand apart, and refuses crossing ones, saying so.

    • 2007 Q1, 2007 Q6 and Planchard 3-3 are one-line placeholders (URL and key) in tools/cswa/README.md.

    703 tests, 0 failures; 44 of 44 mutants killed (26 on the ops and the gate, 18 on the canary flow).

  • v0.11.3: a quick restart is not ready; a reply is not an effect. Two live findings.

    The good one: the HideFileExt fix is PROVEN on Drew's laptop with extensions hidden. Every verb passed by the title, by the stem and by stem + .SLDPRT (titles.hiddenExtensions).

    The bad one, from the ledger: a quit then an immediate start was called ready 4 s after attach. A swx_new_document 11 s after attach returned OK. SOLIDWORKS (pid 26316) died a second later.

    Now:

    • A marker counts only if it is beyond the launch. The performance log's state is recorded when no SOLIDWORKS is writing it (launch, quit, or a first sight of a log still holding the last session). A marker counts only beyond that recorded length, unless the file has been rewritten since (shorter, or different bytes where it ended).
    • A fresh attach (the bridge's own launch, or a young process) is ready only with StartupProcessCompleted TRUE, this session's marker, and SWX_READY_SETTLE_SECS (20 s) since attach.
      • readiness.evidence lists each signal with its age, and readiness.why says what holds it.
      • The transition is ledgered as solidworks_ready.
      • The ready latch stays keyed to (pid, creation time).
    • Verify by effect. After swx_new_document, swx_open and swx_import, the live session is read again: the same process, still running, a non-zero document count, and the document in the list. A fresh session is read again after 1.5 s. Failure is host_app_died (with the pid, the ledger line and the crash markers) or document_not_created, never ok.
    • A start within 60 s of a quit waits (up to 30 s) for that session's sldworks.exe and per-session children to be gone, never the platform daemons. It is ledgered as launch_waited_for_previous_exit.

    691 tests.

  • v0.11.2: the File ▸ Open was the launch's own. The laptop's ledger had the sequence:

    • the 3DEXPERIENCE helper sign-in took 2 min 13 s;
    • the supervisor called that a stall, killed the chain and invoked the shortcut again;
    • SOLIDWORKS came up 6 s later, from the FIRST invoke;
    • the second invoke reached it as an "open" request, and that raised the dialog.

    Now:

    • helperSignIn pauses the stall clocks. Only a dead chain is a stall, and past 600 s the launch STOPS rather than retries.
    • A retry never re-invokes while any sldworks.exe from this launch exists. It reads the census before cleaning up and watches for 15 s before any retry. It ledgers launch_retry_declined and waits to attach instead.
    • Each route invocation is a verb for the dialog census (launch.invoke, launch.retry), with a 90 s grace after it ends. An empty File ▸ Open a route provably raised is cancelled by the launch itself and by swx_start, and ledgered.

    See docs/LAUNCH-RESILIENCE.md. 684 tests.

  • v0.11.1: which verb put that dialog up? Measured 2026-10-15 on adom_drew2:

    • a File ▸ Open dialog appeared during a bridge battery, with Drew away;
    • it was "Open ", #32770, owned by the SOLIDWORKS main frame, with the file name empty;
    • every swx_stop after it was stop_blocked_by_dialog;
    • the census called it kind:"unknown", bridgeCaused:false, and swx_state once reported blockingDialogs: [] while it was up.

    Now:

    • Every verb is bracketed by a dialog census. One census runs before the verb (after it is marked in flight), and one after. A dialog proved absent in the first and present in the second appeared DURING that verb.
      • Replies carry dialogsAppeared, and the ledger records dialog_appeared.
      • Sightings persist (dialog-sightings.json) with first-seen and last-proved-absent times and the user's last input.
      • Anything first seen outside a bracket stays unattributed.
    • The census is native and deterministic. It covers every top-level window of SOLIDWORKS and its CEF children: every #32770, plus visible owned popups. It runs on every swx_state call, screenshot:false included (that path used to skip the census and say []). ab's list is only the fallback. No visible modal is dropped: the quit preconditions report moreBlockingDialogs.
    • swx_stop's refusal names the dialog: title, hwnd, class, owner, rect, ageSeconds, attribution, and whether the bridge may cancel it.
    • The one dialog the bridge may cancel: the standard Windows file dialog (file-name control 1148/1152 and IDCANCEL), with an EMPTY name read by WM_GETTEXT. A bridge verb must provably have raised it: proved absent after the verb began, and no user input since. swx_dismiss_blocking_dialogs {close, confirm, reason} sends IDCANCEL, a no-op, and ledgers dialog_cancelled with the attribution. Anything with text typed, or of unknown origin, is the user's.
    • Quit rung (b) reads swCommands_App_Exit from the installed interop and does not send RunCommand unless it reads 2825. A wrong command id could raise any dialog.

    681 tests.

  • v0.11.0: CSWA phase 1 — exam parts as release validation. Drew: "pull 3-5 random parts from CSWA exams as part of our e2e testing/validation for each release … We need this bridge to be a real, useable tool, for more than the basics." Every new op is built and simulated, and NOT YET MEASURED LIVE (proven.live:false until the coordinator's run).

    • Sketch geometry: line, centerline, polyline, arc (centre and angles), arc3, tangentArc, slot, polygon, rect.cornerRadiusMm, sketchFillet, sketchChamfer, and construction:true on any entity. Composites expand into lines and arcs (CreateLine / CreateArc / CreateCenterLine). Contours are checked CLOSED before anything is drawn, and an open end names its gap. Several closed contours are several regions.
    • Features: revolve / revolve_cut (FeatureRevolve2; the axis is a line of the sketch, two points, or the sketch's x / y; the profile must lie on one side). mirror of features or bodies about a plane, an offset plane, a face, or a plane offset from a face (InsertMirrorFeature2; marks 1, 2 and 256; swFeatureScope_e read from the interop). hole: plain, counterbore, or countersink (a cut drafted at half its angle), as cuts on the face's plane. End conditions upToNext (11), upToSurface (UpToSelection 10, the face at mark 1), offsetFromSurface (5), and draftDeg. A plane offset from a face. set_material {densityKgM3}.
    • swx_mass_properties adds massG, volumeMm3, surfaceAreaMm2, centerOfMassMm, densityKgM3 and materialName.
    • The benchmark is in resources/cswa/ (in the zip): the LEGO brick (spec 0); 3dengr Q5, Q6, Q8 and Q9, with the suite's first centre-of-mass keys; and the official sample exam's Tool Block. Steps 1-3 run with spec variables and extends. Step 4 is skipped: it needs offsetEntities.
      • tools/cswa-check.py runs it live.
      • swx_selftest {"level":"cswa"} runs it anywhere and reports "N of M CSWA parts within tolerance".
      • tools/release.sh --require-live <report> refuses to publish without a passing report from this version, newer than the last code commit.
    • The simulator integrates the solid by columns (mock_csg): every feature is a primitive, added or cut in order. Each column is exact along its axis, and Gauss-Legendre runs between profile breakpoints across it. So a partial cut, like the Tool Block's R41 slot, weighs right. Every runnable spec lands within 0.01 % of its key in the simulator.

    677 tests.

  • v0.10.10: SOLIDWORKS 2021, tested — and the one bug it found. Michael ran 0.10.9's swx_selftest {"level":"write"} on SOLIDWORKS 2021 (COM 29.5.1, Windows 11): 44 passed, 6 failed. Every modelling check passed, and the LEGO brick came out identical to 2026 (4 features, 2073.362 mm³, 39 faces). The 6 failures were ONE bug: the self-test addressed its scratch part as Part14-….SLDPRT, and with Windows' "Hide extensions for known file types" on (the Windows default) SOLIDWORKS titles it Part14-…. The close missed too and left the part open.

    Now:

    • Titles match extension-insensitively everywhere (same_title: a trailing .sldprt/.sldasm/.slddrw, any case, is ignored on both sides; a dot elsewhere, Rev1.2, is part of the name). This covers document resolution, ownership of unsaved documents, closing, activation and the collision checks, in the server and in the COM host.
    • The self-test addresses its own documents by path, retries a missed close by title, and reports documentsLeftOpen instead of leaving a part silently.
    • swx_new_document reports the title SOLIDWORKS shows, read back rather than built from the file name, alongside the path.
    • A cut-off self-test leaves nothing behind. Every document it creates is recorded and struck off when closed. The next swx_selftest closes what a cut-off run left. swx_status.orphanedSelftestDocuments reports them, and closes nothing (a run may be in progress). Two self-tests cannot run at once (selftest_running).
    • swx_selftest replies (and its catalog entry) lead with _timeoutHint: call it with responseMode:"stream" or "job".
    • 2021 is tested. compat::OP_FACTS carries the community measurement (by:"community") for the 32 variants the run measured, so proven.live is true on 2021 for those. The T33+ checks have run on 2026 only. The ownership row is numbered T23.
    • The simulator can hide extensions (mock-hide-file-ext). The full self-test and the six verbs pass both ways.

    639 tests.

  • v0.10.9: the presence rule, made symmetric. Measured 2026-10-14 on hosted 0.10.7, with Drew at the keyboard during a battery:

    • 17 swx_reset_workspace calls in a row were refused for his presence;
    • 18 new scratch parts were allowed and piled up on his screen;
    • checks slowed to a 7-minute stall, SOLIDWORKS reached 15.1 GB, and the machine 91 % committed.

    Now:

    • Creation is guarded too. While the user is at the machine (input within the idle threshold), the self-test's write level and purpose:"bridge" documents and scripts wait for the user to go idle (up to waitForIdleSeconds, default 120), then refuse with user_present having created nothing. A user's own script is still allowed, and its reply says the user is active.
    • Closing the bridge's own scratch is allowed while the user is present, except the active document. swx_reset_workspace reports leftActive. It is refused only when no document qualifies, and each close is ledgered with userPresent:true.
    • The self-test stops (stoppedFor:"userPresent") before a check if the user is still active after the wait. It closes its own part first and never ploughs on.
    • swx_run_script reports documentsLeftOpen with why:"user_present" when it made a document while the user was active.
    • swx_status.userPresence (active, idleSeconds, foreground) is always present, read without COM.

    635 tests.

  • v0.10.8: clear_part in passes. 0.10.7's full live self-test completed:

    • nothing was throttled, and the launch was attributed by ancestry;
    • it created 15 documents instead of more than 32;
    • SOLIDWORKS went from 507 to 2361 MB;
    • 40 passed, 3 failed, 1 skipped.

    The 3 failures were one bug. clear_part's single DeleteSelection2 returned true and left a top-level sketch that an earlier check had left unconsumed (a cut that removed nothing keeps its sketch). Now:

    • clear_part walks the tree again after each delete and deletes what is left. It selects a feature by IFeature::Select2, or by name with its own type (a sketch as SKETCH) when that refuses. It makes up to 5 passes, succeeds only on an empty tree, and reports passes and deleted. The simulator and the real host share the loop.
    • If the part still will not clear, the self-test runs that check in a fresh part and records reuseFallback on the row. It counts the part in documentsCreated, so a check fails only on its own geometry.
    • The simulator now models the leftover sketch.
    • Docs record this run, and that the closing respawn freed 0 MB this time.

    631 tests.

  • v0.10.7: what the live run decided. 0.10.6 was measured on SOLIDWORKS 2026:

    • Hidden documents cannot be modelled. SOLIDWORKS selects only in the active document, and a hidden one cannot be activated.
    • Twelve builds in ONE reused, visible part left SOLIDWORKS flat (3260 to 3246 MB, handles 4373 to 4398). Each new part costs about 85 MB and 125 handles.
    • swx_com_respawn freed about 210 MB while the handles did not drop, so the bridge does not pin documents.
    • The Connected edition's launcher re-parents, so 0.10.6 never recognised its own launch.

    So this release:

    • Visible by default. The self-test, the quit ladder's scratch part, swx_new_document and scripts are all visible. A script that would model a hidden part is refused up front with document_hidden.
    • One visible reused document per battery. The self-test uses it, reports documentsCreated, and ends by respawning the COM host (comRespawn.freedMb). A reuseDocument that cannot be used says so by name. Measured, it read "needs a document", because the host looked only at the active document.
    • Attribution at attach. First ancestry; failing that, the ONE sldworks.exe that was not in the launch's baseline and was created between the attempt's start and the attach. Never with two candidates, never a process other than COM's. Recorded as attributedSolidworks.how:"newProcessInLaunchWindow".
    • No throttling. The self-test carries on when commit is high but SOLIDWORKS is within its budget, and reports it (memoryPressureSeen). It recycles the bridge's own empty session when SOLIDWORKS itself is over budget. It stops only at critical, or when SOLIDWORKS is over budget in a session it may not end. The verb guard is unchanged (critical only).
    • clear_part is now marked measured live.

    628 tests.

  • v0.10.6: fixing the memory growth at its root. Measured 2026-10-14 on 0.10.5 (12 parts built and closed, on a fresh SOLIDWORKS 2026): SOLIDWORKS's private bytes rose 520 MB, 913, 1191, then +83 MB per part to 2035 MB. Handles went 2845 to 4539 (about +125 per part). 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 been built and closed in it. Quitting it returned about 16 GB.

    • swx_status.memory reads the commit charge (GlobalMemoryStatusEx), SOLIDWORKS's private bytes and handles, and the top five processes, with no PowerShell. It reports pressure as ok, high or critical against a 6 GB session budget (SWX_SW_MEMORY_BUDGET_MB). Every reply's _hint names the largest holders while pressure is not ok.
    • At critical, verbs that create or load geometry are refused with low_commit_memory and change nothing. The refusal gives the numbers, the holders and the one safe remedy. ignoreMemoryPressure:true with a reason overrides it, and the override is ledgered.
    • swx_selftest {"level":"write"} reads memory before each check. When needed, it recycles a session the bridge started that has no documents open (recycled). Otherwise it stops with stoppedFor:"memory". It never ends a session that is not its own.
    • Every ledger line and every verb's log line carries a memory sample.
    • The solidworks-memory skill ships as the package's third skill.
    • The eight checks added with 0.10.4 (T33–T39 and T42) are now in the measured table, with their own measurement.

    614 tests.

  • v0.10.4: proven live on SOLIDWORKS 2026. 0.10.3 passed 30 of 30 checks in tools/dsl-live-check.py, and swx_selftest {"level":"write"} passed 43 with none failed (docs/LIVE-RESULTS.md). Every live selection used the first rung, the typed MultiSelect2, and it stays first. Each step's proven is now {"live": …, "measuredOn": [{"solidworks": 2026, "major": 34, …}]} in place of false. live is true only when every variant the step ran was measured on the release it ran on. The variants come from one table in compat.rs, which swx_describe and swx_selftest also report as liveMeasured. Variants no live check has built say so under unmeasured: a reversed boss, through all on a boss, Right Plane and side-face sketches, patterns along other axes, the later selection rungs and the CreateDefinition fillet. The shell result now reports its route like fillet and chamfer. Nothing else changed. 596 tests.

  • v0.10.3: selection after a geometric pick, fixed on SOLIDWORKS 2026. The first live run of tools/dsl-live-check.py (2026, major 34) passed 20 of 30 checks. All 10 failures were one bug: MultiSelect2, given a SAFEARRAY of VARIANTs, selected nothing. This broke face sketches, fillet, chamfer and shell. Selection is now a LADDER, and each rung is believed only when GetSelectedObjectCount2 grows by what was asked:

    1. MultiSelect2 with a typed VT_DISPATCH array;
    2. IEntity::Select4, per entity, through the object's IEntity interface (the IID is read from the installed interop, and the bridge checks the interface is dual first);
    3. SelectByRay with the bridge's own ray, from 1 mm outside the point back onto it, so it does not depend on the view;
    4. last, SelectByID2 at the point, which does depend on the view.

    Each step reports the rung that worked as selectedVia, and viewDependent:true when rung 4 was needed. The simulator now models the measured install, and a fault per rung shows the ladder falling through. ISimpleFilletFeatureData2.Edges gets the typed array too. 592 tests.

  • v0.10.2: Michael's features, made to work on real SOLIDWORKS. An independent review checked every new COM call against the 2021 API help. The signatures, argument counts, units and enum values were right; the risks were behavioural, and each is fixed:

    • faces and edges are picked by GEOMETRY (GetClosestPointOn over every face and edge), never through the view, so a hidden bottom face works;
    • a sketch's coordinates go through each sketch's own transform, so a flipped plane cannot mirror them, and a boss from a face goes outward and a cut inward;
    • an offset plane is found by its tree position (IRefPlane has no name), and its side and an axis's direction are read back;
    • fillet edges are de-duplicated (selecting an edge twice deselects it);
    • a material no longer wipes a colour set earlier, because the bridge re-applies it;
    • the extrude re-selects its sketch by name;
    • the pinned document is activated, and the user's active document is put back;
    • default planes are found by position, so a localized install works;
    • density is mass / volume per body;
    • FeatureFillet3 falls back to CreateFeature.

    Also fixed: ActivateDoc3 was passed 0, which is swUserDecision (a prompt), where 1 (don't rebuild) was meant. A failed script's new part is now owned, so it can be closed. The simulator models every one of these hazards. swx_selftest {level:"write"} is now a 32-check GEOMETRIC proof with expected vs measured on each row; it is the same battery as tools/dsl-live-check.py. 587 tests.

  • v0.10.1: the self-test never reports a closed SOLIDWORKS as a failure, and a Connected install is not called classic. Measured on hosted 0.10.0 with SOLIDWORKS closed: swx_selftest counted swx_docs and swx_get_preferences as failures ("SOLIDWORKS is not running"). Now:

    • every check that needs SOLIDWORKS is skipped, with the reason, and the summary says solidworksRunning: false;
    • the hint's first sentence asks for SOLIDWORKS to be started;
    • level:"write" is refused up front and changes nothing.

    swx_status.solidworks.classicYears said [2026] on the 3DEXPERIENCE 2026 install. registryYears now carries the raw registry years, and classicYears only the years with no 3DEXPERIENCE entry or install path for that release, which is [] on that machine. The edition decision is unchanged. 576 tests.

  • v0.10.0: issue #1, the modelling DSL Michael asked for, and SOLIDWORKS 2021 as a supported release.

    • Sketches. One sketch holds several profiles, so concentric circles are a ring. A sketch can go on an offset plane ({offsetFrom, offsetMm}, made by the script) or on a face ({atPointMm}). anchor:"corner"|"center" and centerMm place rectangles.
    • Extrude and cut. Both take reverse, direction:"both" and midPlane. A feature that comes back null is now an ERROR; it used to be reported as success with nothing built.
    • New ops. fillet, chamfer, shell, linear_pattern, circular_pattern and set_appearance. swx_inspect_bodies reads the colour back.
    • Documented conventions. The help, the reference and the skill state the corner anchor, Top Plane +y → model −Z, and that a cut goes along the −normal.
    • The brick. examples/lego-2x4.json builds Michael's LEGO 2x4 brick in 4 features (his build took 22). The simulator's arithmetic gives 2073.3615 mm³, the volume SOLIDWORKS 2021 measured for his brick. It is still UNMEASURED live.
    • 2021 support. docs/COMPATIBILITY.md audits all 149 COM members the bridge calls against help.solidworks.com. None is newer than 2017, so bridge.json now declares >=2021 (supported) and 2026 (tested).
    • Version facts. swx_status.solidworks carries majorVersion and versionSupport. Version-gated calls refuse needs_solidworks_version by name.
    • Classic installs. Detection recognises a classic desktop install from its SOLIDWORKS <year> registry key, and SWX_PROGID picks one release when several are installed.
    • swx_selftest. It returns one JSON of per-verb pass/fail on any install. read changes nothing; write uses only a scratch part that it closes. It is how a user on a release we have not run gets first-class support.
    • 575 tests.
  • v0.9.29: issue #2 fixed: the mass is right, and the bridge knows its own part. Reported by Michael, our first community user, on SOLIDWORKS 2021. swx_inspect_bodies passed 0.0 to IBody2::GetMassProperties, whose argument is the DENSITY (help.solidworks.com, Availability 2001Plus). Its massKg came back as volume x 1, about 1000x small. Each body's mass now uses the density of an IMassProperty restricted to that body (AddBodies), or the part's if that is unavailable. The reply carries densityKgM3 and densitySource, and the mass matches swx_mass_properties. An unreadable density gives massKg: null. swx_run_script decides bridgeCreatedDocument with the same path-based ownership test as every other verb; it used to compare titles. It now also pins its target by PATH, so a user's document that shares a title with the bridge's part is never mistaken for it. An audit of title-only identity found two more: the registry's forget and rename after a close or Save As now match by path, so closing one document no longer disowns another of the same name. 565 tests.

  • v0.9.28: a launch record that outlived its processes is ended, not attached. Measured 2026-10-12: ten days after a launch, with SOLIDWORKS not running, swx_status.launch still said "up and COM-attached. Carry on." The record is now checked against the live processes (pid plus creation time, so a reused pid does not count) and reports stage:"ended", endedWhy and honest advice. The skills also say where signInWindow lives: at the top level of swx_status, beside waitingOn.

  • v0.9.27: the local sign-in helper. ab hands a saved password only to a local caller, never to a bridge or over the relay, and a cloud agent is not allowed to start that local caller either (both measured 2026-10-12). So the page now ships tools/local-sign-in.ps1: run on the PC, it asks the vault behind ab's consent card, sets the password into the login window, ticks Remember me, clicks Log in and verifies. The sign-in skill says exactly who may touch the password and how. Docs and a tool; no runtime change.

  • v0.9.26: page 2, read right. Measured on the live window, which was on page 2 with the name filled. 0.9.25 called it page 1 because the user-name box is there too. It tried to set that box, which is prefilled and read-only, and blamed the refusal on a cookie banner that was really a "Manage cookies" link. It toasted Drew to answer that banner and never asked for the password. Now a page with the password box is page 2, a read-only name counts as prefilled, and only accept/reject buttons make a banner. A failed set is never blamed on a banner, and "Remember me" is toggled in the background and reported from its read-back. 561 tests.

  • v0.9.25: the page is prepared; the password stays with the user. Measured on the live window: ab refuses pup_credential_reveal to a bridge ("requires a VERIFIED local transport … origin "bridge""). That is ab's policy, and the bridge does not work around it. swx_sign_in now fills the user name (from the vault's host list, no secret) and presses Continue BEFORE it asks for the password. A refused reveal is sign_in_vault_policy, with ab's words (abSaid), pagePrepared {page, userNameFilled} and waysOut: the user types the password, or ab issue #237 lands a fill verb that keeps the secret inside ab. Never retried. 560 tests.

  • v0.9.24: the vault answers. Measured on the live sign-in window: pup verbs reply error:"" (an empty string) on success, and the bridge read any non-null error as a failure, so swx_sign_in reported the vault unavailable with an empty message. An error is now a non-empty string, or success:false / ok:false. One line; no other behaviour changed.

  • v0.9.23: a third sign-in host, found by content. After a week away (2026-10-12) the platform updater SWXDesktopUpdate.exe showed the 3DEXPERIENCE ID login in a window titled just "3DEXPERIENCE". The bridge saw nothing: swx_start {app:"visualize"} said "launching" and swx_status said waitingOn:null. It also refused the window when the user named it. Now a login is recognised by its content (an Edit "Email or username" or "Password"), the updater is in the census, and swx_status / swx_start (both apps) report waitingOn:"userSignIn" with signInWindow {hwnd, title, ownerImage, host:"desktopUpdate"}. That field is now an object; it used to be a title. A window the user names is probed by content and never refused on census grounds. When the tree holds only the frame (measured: 14 nodes), swx_sign_in finds each control with desktop_find_control: by automation id first, never a role alone. It clears each field before setting it, which fixes the measured stray trailing "s". 558 tests.

  • v0.9.22: the skill carries the whole job. The public solidworks-bridge skill's everyday flow now covers rendering (swx_render_image, the measured recipe and zoomFactor), SOLIDWORKS Visualize (swx_start {app:"visualize"}, the first-run folder dialog that is the user's to answer), pulling exports back, and keeping the bridge current (refresh_bridges). Nothing about this bridge is private: what remains in any user's own pack is their secret, their machine and their authorizations. Docs only; no runtime change.

  • v0.9.21: the sign-in skill says when Visualize asks. Started through its platform shortcut while the 3DEXPERIENCE Launcher holds the session, Visualize raises no login (measured 2026-10-05); the separate login seen on 10-02 came from starting its exe directly, which this edition refuses. Docs only; no runtime change.

  • v0.9.20: a setting SOLIDWORKS ignores is not "applied". The second live render (4K, 11.2 s, zoomFactor:2.0) showed RealView's put returning OK and the read-back staying false (uncertified RTX 4090 Laptop GPU); 0.9.19 still called it applied. Every set is now read back: ignored → why:"setIgnored", changed:false, restored:null, plus a notes[] sentence; no machine-wide workaround is applied. zoomFactor measured (>1 zooms in; 2.0 clipped the Moveo — 1.4–1.6 after a fit). swconst ids recorded (AO 485, shadows 131 on 2026 SP3). swx_status.visualize was measured reading the real WPF first-run dialog; it now reports the top-level hwnd whenever Visualize has its main window — on first run the dialog IS that window. 553 tests.

  • v0.9.19: the render is measured; Visualize can be started. The render ran live on 2026-10-05 (4K in 11.6 s, minimized window, every setting restored). It now accounts for every requested setting (settings: already as asked vs applied vs unreadable — the live run could not tell about shadows), takes a zoomFactor (the fit left the assembly at ~25% of a 16:9 frame), and reports the swconst ids it read. swx_start {app:"visualize"} starts SOLIDWORKS Visualize through its platform shortcut (a direct start is refused by this edition — measured) and reports, never answers, its dialogs: the platform notice, the first-run content folder, the login. 552 tests.

  • v0.9.18: the bridge can render. Drew asked for a gorgeous shot of an assembly. swx_render_image draws the active document with SOLIDWORKS's own viewport (RealView, ambient occlusion, shadows, perspective, a plain background) at any size up to 8192 px a side through SaveBMP, converts it to PNG, and reads the size back from the file (capped:true if only the window size came out). Every display setting it changes is captured and restored — on errors too — and listed; the document is never saved; a render that changes the screen waits for the user to be away. The swconst ids it needs are read from the installed interop DLL, never guessed. Visualize (track B) is designed in docs/RENDERING.md, to be measured first. The COM path is UNMEASURED. 545 tests.

  • v0.9.17: the resize is the measured one. The window-tall-enough step that clears the cookie banner now calls desktop_set_window_bounds {hwnd, x:300, y:30, w:1100, h:1450} — the call that was measured to work on 2026-10-02 (email box at y=787, banner at y=1020) — instead of a resize verb that had never been observed on ab. No other behaviour changed.

  • v0.9.16: any login window, typed where it really is. Measured on the Visualize login window: page 2 (its "Remember me" checkbox is off-screen, so its visible label is what gets clicked), the page re-lays when the window moves (clicks now land at the field's own fresh centre, and a banner over the field is never clicked through), and a click only takes focus after the window is raised. Typing a password from the cloud is refused by policy, so the bridge — which reads the vault and types on the desktop — now answers ANY 3DEXPERIENCE ID login window on the user's say-so (userAuthorized:true, window:"visualize"|"solidworks"|<hwnd>), naming the process that owns it. 536 tests.

  • v0.9.15: the real login window, as measured. The first real 3DEXPERIENCE ID window (raised by Visualize) was measured on 2026-10-02: its accessibility tree is lazy (13 nodes until a point is queried, then 38), the form is two pages (user name + Continue, then the password), a cookie banner sits on it, and it refuses a background text set. swx_sign_in is now one flow over both pages with ONE consent, wakes the tree, never touches the cookie banner, and types real keystrokes only safely — never while the user is typing (a measured collision put the user's keystrokes in the box), only with the login window in front, select-all first, the user name read back before Continue. A missing login names every way to grow the vault. A second skill, solidworks-sign-in, ships the measured flow for every agent — with no credentials. 533 tests.

  • v0.9.14: the sign-in speaks ab's measured dialect. Before the first real sign-in, the three ab verbs it depends on were measured on Drew's laptop: desktop_ui_set takes text (0.9.13 sent value — every fill would have failed), the UI tree's nodes carry role/invokable/settable and no password flag (the password edit is now found by its name; a truncated tree is refused), clicks need a name, and the vault's reveal is gated by a consent toast the user clicks every time — waited for, with a plain sentence for the user, and relay_forbidden reported as a wiring bug. A missing login is found from the vault's host list and never raises the toast. 523 tests.

  • v0.9.13: the bridge can sign you in. Drew asked for the 3DEXPERIENCE sign-in not to need him every time the platform session lapses. Every time a launch reaches that window its accessible inventory is captured (never a value); swx_sign_in recognizes the form from it, takes the login from the user's own vault on their laptop, types it through UI Automation, ticks "Remember me", and verifies the window closed — the password never appears in a reply, log or ledger, and a rejected one is never retried. swx_start {signIn:"auto"} does it unattended; the default still waits. A respawned bridge sees a sign-in from a live census, and a record left at helperSignIn is reconciled once SOLIDWORKS attaches. 521 tests.

  • v0.9.12: an assembly session closes completely. 0.9.11 was proven LIVE on 2026-10-02 on Drew's 34-document crate session (lock-out fixed; the presence guard refused the sweep until Drew's own words overrode it; Part1 preserved, 33 closed in place untouched; quit 3.5 s) — and the sweep could not close it unaided: parts listed first were "still open after CloseDoc" behind their assembly. The sweep now preserves everything first, closes in a global order (assemblies, the active one first, before parts), defers what is held behind an open assembly and counts what went out with its parent. Sweeping verbs declare ab budgets longer than the sweep (ab had cut a reply at 120 s), swx_progress shows a sweep's stage and counts, a task dialog whose words cannot be read says so, a relaunch waiting on the user's sign-in says waitingOn:"userSignIn" (the launch gate exits 4), and tools/release.sh --publish is the whole wiki sync in one command, verified end to end. 510 tests.

  • v0.9.11: a session cannot be starting for hours. Measured LIVE on 2026-10-01: Drew's three-hour-old session with 34 documents open was locked out of every gated verb after an update respawned the bridge — the log's "UNLOAD_ADDIN" "SWK3DCompassAddin.dll" line contains LOAD_ADDIN and was read as the platform add-in loading. Load events are now parsed as the exact token (the measured log is a fixture); a process older than ten minutes with COM's start complete holds "loading" only while startup lines are still changing; periodic <PROCMEM> lines no longer count as startup activity; the ready latch is persisted across bridge restarts; and swx_user_presence is exempt from the startup gate, reporting documents as unknown until the session is verified ready. 502 tests.

  • v0.9.10: the bridge's documents are only the ones it opened. Adversary pass 46 found the user's own documents could enter the presence guard's baseline three ways (a session the user launched, a document opened before the bridge's first verb, and any later bridge verb re-reading the open set) — each measured sweeping the user's documents. The baseline now starts empty for every session and grows only by what a bridge verb itself opened or created; in a session the user launched every document is theirs, so quit and sweep refuse user_is_active without the explicit override. No foreground window (UAC prompt, lock screen) counts as present; override ledger lines name the caller; the re-check before priming is pinned mid-verb. 497 tests.

  • v0.9.9: never end a session the user is working in. 0.9.8 was measured LIVE on 2026-10-01 (launch 16/16, drain 5/5, quit gate 11/11) — and then Drew started using the bridge-launched session: a "leave as found" quit met his File Open dialog, and minutes later the session held his 34-document crate assembly. Every quit and sweep (and priming) now measures USER PRESENCE first — SOLIDWORKS in the foreground with input in the last 120 s, documents the bridge did not open, or an earlier take-over — and refuses user_is_active with the evidence; unreadable counts as present; positive evidence marks the session taken over for good, and only userPresentOverride + userAuthorized + a reason proceeds. The gates refuse on presence and never override it. Adversary pass 45: an unreadable performance log is unverifiable, never "no log", and the ready latch is keyed by pid + creation time. 491 tests.

  • v0.9.8: modality is a persistent measurement. 0.9.7 was LIVE-PROVEN on 2026-10-01 (launch 16/16, READY 16.7 s with the log read as rewritten-per-start and complete; drain 5/5; quit gate 11/11; primed quit 3.4 s). One new measurement: the 3DEXPERIENCE Connected notice, read at an instant when SOLIDWORKS was busy inside a sweep's own close, had a DISABLED owner frame and stopped the sweep as a modal. When the owner's state is the ONLY fact that makes a window blocking, the census now re-reads it (5 reads, ~2 s): disabled throughout is still a modal; enabled later is a notification reported with ownerDisabledTransiently:true and the read history. A window with a button, button-like captions or save/recovery/import words is never settled. 482 tests.

  • v0.9.7: no marker history, no lock-out. 0.9.6 was LIVE-PROVEN on 2026-10-01: launch gate 16/16 (READY 32.3 s), gated replies carrying their readiness, sweep PASS, drain 5/5, quit gate 11/11 printing the bridge's dialog census, and priming fixed live twice (declined 30.7 s → primed quit 3.4 s). Adversary pass 44: the perflog-markers-seen flag is removed — a session that never loads the platform add-in (SWK3DCompassAddin.dll, which a user may disable) is ready once its log is quiet and COM agrees, and a log the session is still writing holds the start even on a machine's first launch; a platform load whose marker never comes stays starting and the timeout says why; the log's shape (rewritten or appended) is recorded; swx_stop's startup carve-out is pinned to userAuthorized; underscore-prefixed arguments are stripped from caller input; and swx_dismiss_blocking_dialogs reads a window's words with the same classifier as judge_window. 479 tests.

  • v0.9.6: priming fires again. 0.9.5 was measured LIVE on 2026-10-01 (launch gate 16/16, swx_docs refused during startup, sweep PASS, drain 5/5) and regressed one thing: a document count read during startup recorded a never-driven session as having held a document, and the history guard then refused to prime — the quit was declined twice. The session history is now ADVISORY (priming triggers on the declined, empty, dialog-free, authorized quit itself), and it records what each poll saw: never from a count read while starting, and only from a count that persists across two polls or from the bridge's own create/open. The quit gate judges dialogs by the bridge's verdict (a notification no longer refuses its baseline); every gated verb's reply carries the readiness its gate evaluated; the performance log is read from its per-start header, and the on-demand rule keys on the platform add-in's DLL (SWK3DCompassAddin.dll). 473 tests.

  • v0.9.5: the gates say why they failed. 0.9.4 was LIVE-PROVEN on 2026-10-01: the notification rule (a quit in 5.1 s with the 3DEXPERIENCE notice on screen), priming (declined at 130 s, primed, quit in 3.4 s), and the full cycle (launch, ownership sweep, drain 5/5, quit gate exit 0). The age data refines the priming rule: never-had-a-document sessions quit unprimed at ~25 s and declined from 50 s to 8 min — measured ranges, recorded as such. The "transient" fast empty shell_execute failure was never transient (a curl to a dead port on 09-30; the launch gate's UIA probe on 10-01, every time): every gate script is now wrapped to print its own error text and native exit code, the UIA capture says why it has nothing and can no longer abort a gate whose claims passed, and swx_status exposes solidworks.sessionHistory (everHadDocument true/false/null with its basis) so a caller can predict a priming. Folded in, adversary pass 42: the startup gate is now the DEFAULT — every verb that reaches SOLIDWORKS waits unless it is on a pinned 16-verb exemption list (swx_show_*, the export verbs and swx_com_eval had been reachable during add-in load); the performance log only speaks for the session it was written in (a log older than the process is never "complete"), COM alone never decides on a machine whose log writes markers, and an add-in loaded on demand after the start no longer leaves a session "starting" forever; the grace unlocks READ verbs only; an authorized quit reaches the window-message rungs during startup; a non-zero document count at any poll records the session's history; and a prime whose close fails names the part to drain. And adversary pass 43: a window's WORDS now count when judging a dialog — a save, recovery or import prompt in its text blocks, two or more button-like captions drawn in any control class block (the measured template placeholders are not button-like), and measured notification evidence beats a licence-type title alone. 470 tests.

  • v0.9.4: a notification is not a modal. Measured 2026-10-01 on hosted 0.9.3 (where an ab update was also measured to leave the data root byte-for-byte untouched, and readiness worked — with StartupProcessCompleted TRUE before the log marker, so both signals stay required): the 3DEXPERIENCE "Connected to …" notice — a title-less #32770 tool window whose owner frame stayed enabled for 95 s and which had no Button-class control — blocked every quit, sweep and import for minutes. The window census now records evidence for every candidate (owner, ownerEnabled, style, exstyle, rect, child control classes and visibility) and returns it in every dialog report; a window blocks when its kind is known-blocking, its owner frame is disabled, or it offers a Button-class control, and is a reported, never-answered notification only when all of that was measured otherwise — anything unread fails closed. Template placeholder captions are flagged (textIsTemplatePlaceholder) and not presented as the window's words; the quit gate prints full evidence when blocked. neverAnswer is unchanged. 452 tests.

  • v0.9.3: attached is not ready — and a declined quit is about the session's history. Both measured live on 2026-10-01.

    Document commands sent 8 s after COM attach CRASHED SOLIDWORKS while its 3DEXPERIENCE add-in was still initialising; a control launch that sent none crashed nothing. So a READY stage now follows attached: COM StartupProcessCompleted and the performance log's 3DEXPERIENCE marker (after the last LOAD_ADDIN line) must agree where they can be read; with neither, a 60 s grace stands in and says so; nothing is ever promoted to ready by the clock. swx_start reports stage:"ready" only then, swx_status exposes solidworks.ready and what it rests on, and fifteen document and quit verbs refuse host_app_starting with retryAfterSeconds until it is true. A sweep during which SOLIDWORKS dies now says host_app_gone — preserved and closed counts, and the session's crash markers from the performance log — instead of dialog_census_unknown; SOLIDWORKS' ~$ lock files are listed apart.

    swx_stop rung (a) was declined three times on one session that had never held a document, and quit in 3.4 s once a scratch part had been created and closed in it — reconciling 09-08 and 09-17. The bridge now records per session whether it has ever held a document, and under userAuthorized, on an empty session with no dialog, PRIMES a declined one (one bridge-owned scratch part, ledgered quit_prime) and asks again. The quit gate's "no longer running" claim had passed vacuously after a decline; it now requires running:false and is self-tested from the suite. The gates retry ab's fast empty shell_execute failure up to three times, 2 s apart (it came twice in a row), and the launch gate probes nothing before READY.

    Adversary pass 41: the ledger merge is a MULTISET now (two identical lines in one file are two events — max of the two counts, never fewer), every ledger line carries a monotonically increasing seq, and the cap never cuts silently: the live ledger is trimmed only past twice the cap, and the lines it removes go to ledger-archive-<stamp>.jsonl with a ledger_archived line saying how many and where. 448 tests.

  • v0.9.2: two versions of the ledger or the ownership registry are MERGED, not picked; a same-size, same-mtime rewrite is caught by its hash; and a recovery folder that cannot be made is refused once, before the first save (adversary pass 40; sev-1 count zero, all seven of its mutants caught).

    0.9.1 settled a clash between the two older locations by giving the real name to the OLDER ROOT's copy and putting the other beside it. On exactly Drew's timeline that is backwards — 0.8.9 re-created the cache's ledger after 0.9.0 had copied it and kept appending — so the live ledger.jsonl held only the cache era and the 0.9.0 era sat in a side file swx_action_ledger never reads. "Newest mtime wins" fails the mirror case, where the two ledgers are DIVERGENT. So nothing is picked: the live ledger becomes the union of both (each exact line once, ordered by at), the live ownership registry the union of both claim lists (restore re-validates every claim against the live session), BOTH versions are kept beside the merged file, and the merge is recorded in migrated-from-legacy.json (merged), in the ledger (legacy_files_merged) and in swx_recover_list. The same merge applies when a legacy ledger or registry changes after its copy, so a rollback's entries reach the live ledger too. A version that cannot be merged is kept beside and reported (mergeFailed); every other file keeps the beside rule.

    Change detection hashed only the COPY. A restore that puts timestamps back — File History, robocopy /COPY:DAT, a sync client — can return different bytes of the SAME length under the SAME mtime, and equal length is normal for the ownership registry. Now a legacy file up to 16 MiB whose size and mtime match is hashed and compared with the digest recorded when it was copied; larger files keep size and mtime, and every record says which (comparedBy).

    And recovery_dir() dropped create_dir_all's error and returned the path anyway, so a sweep on a blocked recovery location made one doomed SaveAs3 per document. It now refuses once, before the first save, with recovery_dir_unwritable and the cause. 433 tests.

  • v0.9.1: the data root moves out of ab's directory altogether — and a legacy file that changes after its copy is never silently settled again (adversary pass 39). 0.9.0 moved recovery out of the directory ab re-extracts on every bridge update and INTO %LOCALAPPDATA%\Adom Bridge\solidworks-data\ — inside the directory ab's own installer owns ("installs to %LOCALAPPDATA%\Adom Bridge", "the uninstaller lives at …\uninstall.exe", per ab's docs), whose fate on an ab uninstall, repair or "delete the folder and reinstall" is documented nowhere and measured never. The only copy of unsaved work cannot hang on that: it is the measured SEV-1's shape, one level up. So the data root is now %LOCALAPPDATA%\adom-solidworks-bridge\, a SIBLING of Adom Bridge\ that only this bridge writes to (%USERPROFILE%\adom-solidworks-bridge\ if LOCALAPPDATA is missing), and a sweep refuses (recovery_inside_install_dir) if its recovery folder would land under the install directory, ab's own directory, or either older location.

    Both older locations are LEGACY ROOTS now, migrated oldest first — 0.8.x's bridge cache, then 0.9.0's solidworks-data — under the same rules (temporary name, byte check, rename, never over an existing file). And each copy is RECORDED by (legacy root, path, size, mtime, sha256 of the copy): 0.9.0 settled a file by its name, so a rollback's appends to the old ledger and a rewritten ownership registry were never looked at again. A legacy file that changes after its copy is now copied AGAIN, beside the first copy (<name>.legacy-<stamp>), never over it, and reported changedSinceCopy — in migrated-from-legacy.json, in the ledger (legacy_file_changed_since_copy), and in swx_recover_list's migration block. A same-named file from the other legacy root with different bytes is kept beside too, never dropped.

    With neither LOCALAPPDATA nor USERPROFILE the root would fall back to the OS temp directory, which Windows empties on a schedule: every sweep now REFUSES there (data_root_is_temp, nothing written, nothing closed) and the start-up log says why. swx_recover_list reads all three locations (legacyRecoveryRoots, location:"legacyDataRoot", atRisk), keeps notCopiedYet at verified:null (the adversary's pin), and says ONLY A PARTIAL COPY EXISTS — not "gone" — for a file whose copy was interrupted. SKILL.md is cut to the rules that never bend, with WHEN/trigger cues in its description, and the detail moved into four references/*.md files that install beside it. 423 tests.

  • v0.9.0: the user's rescued work no longer lives where an update deletes it — SEV-1, measured live 2026-09-30. ab's install cache for this bridge, bridges-cache\solidworks, was found CREATED 2026-09-18 09:42 by the auto-install of 0.8.9: the updater re-extracts the whole directory, and that directory was also this bridge's data root. Every recovery folder from 2026-09-17 went with it, with the ledger, the ownership registry and the scratch documents. Only test documents were in them. The recovery folder exists to hold the ONLY copy of work the user had not saved; it cannot live in a directory a routine update empties.

    So the data root is now %LOCALAPPDATA%\Adom Bridge\solidworks-data\, a sibling of the install cache. The first start MIGRATES what an older bridge left there — the recovery folders, the ledger, the ownership and launch registries (with their previous generations), timings, settings and exports — copied, verified byte for byte (streamed), never over an existing file, the originals LEFT where they were, and each file ONCE: migrated-from-legacy.json is cumulative, so a copy the user deletes stays deleted, while a copy that failed verification (and was removed, so no half-copied rescue ever looks whole) is retried at the next start. Every copy is made under a temporary name and renamed into place only once it verifies, so a start that dies mid-copy — a crash, or ab killing a bridge slow to answer /status — never leaves a truncated rescue under the real name, where never-overwrite would protect it forever. The migration runs BEFORE the bridge reads its own state, so the first start after the move already owns what the older bridge created. swx_recover_list still reads the old location, marks each sweep there atRiskFromUpdate, and says whether a verified copy exists (copiedTo) or that folder is the only one; in the data root, a file whose copy has not landed is notCopiedYet with its legacyPath — never "gone" — and a half-copied temporary whose original has since disappeared is a partialCopy: never a rescue, and never hidden. And a sweep REFUSES — recovery_inside_install_dir, before writing or closing anything — if its recovery folder would land under the install directory or ab's install cache, however the path is spelled (links, .., case, either separator). One limit, stated: the update that installs 0.9.0 re-extracts the old directory BEFORE 0.9.0 first runs, so on a machine ab updates from 0.8.9 the migration will most likely find nothing. The move is what protects every update after this one.

    Also: a clean, saved document is RECORDED as closed in place. Its own file is its preservation, so the sweep wrote no copy — and the manifest had no entry for it, so swx_recover_list could not say "your file was closed at its own path, untouched" (measured live 2026-09-30 on Drew's R_0603_1608Metric_1.step-1.sldprt). The sweep now records such a document as closedInPlace with the original's size and mtime, and each listed sweep carries closedInPlace entries that MEASURE the file: untouched, changed since, or no longer at its path. A pre-0.9.0 record is read too, with its close reported as unknown rather than the false it wrote.

    And the live gates now reach the bridge the way ab runs it: through adom-bridge <verb> routing to the bridge ab hosts, since ab 2.1.163 terminates a side-loaded dev copy on 24695 within 30 s (SWX_BRIDGE_URL forces the old loopback route); the quit gate passes userAuthorized:true, the standing authorization, instead of the write-gate pair. The full quit cycle PASSED LIVE 2026-09-30 through the hosted bridge (docs/QUIT-INVESTIGATION.md §5.14). A release is now a deploy: ab hosts whatever the published manifest names. 413 tests.

  • v0.8.9: the never-ask rule defaults to governed, like everything else here. 0.8.8's per-file policy still leaked twice: a blockquoted ask in a docs/ RUNBOOK slipped, because a runbook instructs even though it lives under docs, and skills/**/SKILL.md was never scanned at all. Both are the same shape — a rule stated as "these files are governed" is wrong by omission the moment somebody adds a file. So it is inverted, matching every other default in this bridge (unknown kind takes the strictest floor, an unreadable census is not permission, an unverifiable destination is not free): every .md, .rs and .py in the repo is scanned and NOTHING is stripped, with a short allowlist of files that DESCRIBE — the README and the named reports under docs/ — where quoted and code spans are. A new file is governed until somebody says otherwise.

    Also: a sweep's manifest now records its FAILURES, with whatever structure the host gave. A host failure is the case where the folder holds fewer files than there were documents, so it is the one thing a reader most needs explained — and the record was being deleted precisely then, because nothing had been preserved. A timeout, which kills the com host before it can answer, reports its own facts rather than a null that cannot be told from having thrown the detail away. 402 tests.

  • v0.8.8: the fix for the changelog blinded the file an agent follows. 0.8.7 taught the never-ask pin to skip quoted text, code spans and blockquotes in every markdown file, so a changelog could record the sentences it catches. Measured afterwards: in SKILL.md a blockquoted instruction and a backticked one both slipped, while the same sentence in plain prose was caught. SKILL.md is not documentation about the bridge — it is what an agent LOADS AND FOLLOWS, and markdown emphasis there is presentational. A model reading a blockquote does not conclude the sentence is an example.

    So the policy is per FILE, never per syntax: README.md and docs/** describe, and their quoted spans are stripped; SKILL.md strips NOTHING. Both of the adversary's SKILL.md sentences are pinned as must-catch, and the changelog quote above stays allowed.

    Three guards were also shipping unpinned, each proven by a surviving mutation: write_manifest's verified hard-coded to true, host_failure_entry dropping hostError, and acknowledge_only loosened to one-or-more buttons. The first two are the pass-29 and pass-30 fixes — the manifest is the permanent record of WHY a document was not preserved — and nothing could tell, because every sweep test preserved everything. A refused-document sweep now pins both. 400 tests.

  • v0.8.7: the changelog broke the test that reads it. The 0.8.6 entry below quotes both bypass sentences as examples of what the never-ask pin must catch — and the pin read them as violations, so master was RED on a clean checkout. I measured the suite green and then wrote that paragraph, which is the whole lesson: a documentation edit after the last test run is an untested change.

    The fix is not an exemption phrase and is certainly not editing the changelog until it stops saying what was fixed. The pin now understands a CATEGORY — quoting rather than instructing — and skips markdown blockquotes, fenced and inline code spans, and quoted strings, so example violations live in quotes and in the cases table. Both are pinned: a quoted example does not trip it, and the same sentence unquoted does.

    The rule also turned out to be one-directional: "Close the documents (ask the user)." slipped, because an imperative may lead with its object. It is now order-independent within a statement, while the rule stating itself is still allowed, and all five of the adversary's sentences are rows in the table. And the drain gate's twin opt-in — the one place that tool overrides the bridge's ownership answer — is now scoped to the SOLIDWORKS session recorded in the run manifest, not to the path alone: a twin from another session has its file removed but is never force-closed. 396 tests.

  • v0.8.6: the gate's cleanup can find the runs that died, and cannot be pointed at anything else. 0.8.5 gave every run a unique scratch ROOT, which fixed "earlier runs look foreign" by making them invisible: the manifest scan looked inside the fresh directory, found only its own, and adopted nothing — so a SIGKILL, a crash or a reboot, exactly what atexit cannot cover, left a directory behind per occurrence with no run that would ever collect it. There is now one fixed parent with a per-run subdirectory under it, and the scan reads the parent.

    That made a latent hazard reachable, so both landed together: cleanup asked for allowUserWrites + confirm on EVERY path a manifest named, and a manifest is just a file on disk. Containment now gates the OPT-IN — the twin, the only document that needs it, must lie under the directory this gate owns — while the bridge-created documents are closed in the DEFAULT posture, where the bridge's own ownership check is the proof. Anything outside is refused, named, and raises the exit code.

    And the never-ask pin stopped being a pile of phrases. It had grown eight exemptions so it would pass over this project's own commentary, and two were working bypasses — "The sweep passed. Ask the user to close the remaining documents." and "After the restart, ask the user to close the leftover documents." both slipped through. The rule is now positional (the close must follow the ask, inside the same request) and pinned by a table of example sentences, carrying both bypasses as must-catch cases. 393 tests.

  • v0.8.5: a refusal that asks instead of instructing, and four smaller honesty fixes. interconnect_blocks_import told its reader to "Turn it off in SOLIDWORKS: Tools > Options > …" — a runbook, and an agent holding a desktop bridge can execute a runbook. It now says to ASK the user, and a test fails if any text on that code becomes imperative about changing a setting. verified no longer survives a manifest entry that cannot be TIED to the bytes on disk: a missing or null recoveryBytes was read as agreement, so a verdict about one file was handed to another — and verified:true means the document was closed, so that file is the only copy. The manifest's VERSION is checked, not just the shape of files. A coded refusal no longer carries the generic "if a dialog is up, dismiss it and retry" hint, which was wrong for every one of them. The recovery folder is claimed with create_dir — the creation IS the claim, where check-then-create raced. GATE PARTIAL exits 3, not 0, because a caller reading the status could not tell an untested half from a pass.

    And the live measurement from the drain gate is recorded: SOLIDWORKS REFUSED a second open of a same-titled saved document with errors=65536 (swFileWithSameTitleAlreadyOpen), so a title collision is impossible for saved documents — only pathless ones can collide, which is exactly the state identity-at-birth removes — recorded beside the guard it justifies, in com.rs, dated, with the invocation that produced it.

    The drain gate also stopped leaving its own mess behind. Cleanup ran only on the success path, so a refusal — which is EVERY run on that machine while the License Manager dialog is up — left the gate's documents open in Drew's session and its files on disk; and the leftover check called the gate's own copies foreign, which is what stopped it touching them. It now keeps a per-run manifest, written before each creation, runs cleanup from a finally AND an atexit, and uses a scratch directory unique per run. Verified locally: a passing run and a FAILING run both end with zero documents open and zero directories left. 388 tests.

  • v0.8.4: the gate stopped asking Drew to close things, and a dialog now says what it says. tools/quit-gate.py refused a live run with "Ask Drew to close them himself" — the exact sentence Drew's standing directive exists to delete, still sitting in a tool a week after the sweep that makes it unnecessary shipped and ran. The gate now runs swx_close_all_safely (which preserves every document to a verified recovery folder before closing) and refuses only if the SWEEP refuses, saying which of its reasons applied. Every other "ask the user to close" in the tree is gone, and a test now fails if one comes back — a directive that lives only in the code it prompted gets violated by everything around it.

    And the blocking-dialog report carries the dialog's own words. Live, the sweep stopped correctly on {"kind":"license","title":"License Manager"} and that was all a caller got; the window actually said "Failed to check 1 license(s) … Unable to connect to server. The session will end after the next 5 unsuccessful attempts" with one button, OK. stoppedByDialog now carries text, buttons, contentsReadable and acknowledgeOnly — read-only, null when unreadable, and neverAnswer stays true regardless: whether this bridge may ever press an OK-only informational dialog is a question for Drew, not an inference from a control tree. The measured fact: that dialog appears when the 3DEXPERIENCE licence heartbeat fails (it coincided with the desktop dropping off the relay), and both the sweep and the quit are blocked while it is up. 376 tests.

  • v0.8.3: the gate's own bugs, and a way to have caught them here. tools/drain-gate.py --fixture new ran live and the registry drain PASSED — swx_reset_workspace closed exactly the two documents the bridge had made and left every pre-existing one open. The three reported failures were all the gate's: it asked for keepUnsaved:true and then failed the bridge for handing back a pathless document; it saved the twin to a path with no extension and reported the resulting FORMAT refusal as a write-gate bug; and it tried to close a twin that had never been created, printing an empty title. All four are fixed, and the classification rule is now explicit — classify by errorCode, never by "it failed": a gate that cannot tell its own bug from the bridge's is worse than no gate. The real lesson is that none of it could be reproduced without the desktop, so the gate gained --local URL: it can now be run end to end against a mock bridge on this machine. It was, and all five claims pass. 372 tests.

  • v0.8.2: the import refuses honestly, naming the setting. The matrix is measured, and the answer is that on this edition with 3D Interconnect ON there is no programmatic neutral-file import route at all — LoadFile4 returns nothing whether it is handed Nothing or a real GetImportFileData object, LoadFile2 returns false, OpenDoc6 answers "requires repair" for every option value tried. The imports that worked from 09-05 to 09-11 must have run with the toggle off. So auto makes ONE LoadFile4 attempt — a future edition that accepts it will simply work — and then fails with interconnect_blocks_import, naming the exact setting (Tools > Options > System Options > Import > Enable 3D Interconnect), the measurement date, and the fact that this bridge does not change user settings, not even temporarily. Reporting a translator failure instead would send the reader hunting a missing translator that is not the problem. tools/drain-gate.py --fixture new proves the same ownership and same-title collision properties with swx_new_document, since the STEP fixture cannot be created on that machine at all — and it states plainly that swx_close_import_temps is import-temp-BY-PATH by definition, so the registry drain is the right verb there. 366 tests.

  • v0.8.1: a measurement surface, because no import route is known to work — and two honesty fixes in the rescue record. 0.8.0 claimed 3D Interconnect meant "OpenDoc6 with swDocPART"; live, that returns no document with errors = 2097152 (swFileRequiresRepairError) on two real STEP files. That claim was recalled, not measured, and it is now marked as what it is. What IS measured: toggle 691 is TRUE, LoadFile4 executes and returns nothing (errors = 1), LoadFile2 returns false (still unexplained). PowerShell cannot try alternatives — SOLIDWORKS Connected does not expose the type info its COM adapter needs, so every member call fails TYPE_E_ELEMENTNOTFOUND — which makes this bridge the only instrument. So swx_import takes route, openOptions, docType, withImportData and argString, echoes the exact invocation on every reply, and refuses an unknown route rather than quietly measuring something else. It is inside the verb that already owns and registers what it opens, not a new escape hatch.

    Also: a manifest verdict no longer outlives the bytes it was about — if the file changed since the sweep, the listing says UNKNOWN instead of repeating a verdict about bytes that are gone. Host failures inside the sweep keep their structured COM detail, which since 0.7.8 is the permanent record of why a document was not preserved. A throwing import re-counts and TAKES OWNERSHIP of a document that appeared anyway, instead of leaving it open and untracked. And two sweeps in the same second now get separate folders: they used to share one, so the second sweep's manifest overwrote the first's — a real reporting defect, and the mechanism behind an intermittent test failure that was fixed rather than retried. 365 tests.

  • v0.8.0: 3D Interconnect — the import had been taking the wrong road all along, and the user's setting decides which road exists. With 0.7.9's encoding fix, LoadFile4 finally executed on the live install and still returned no document, errors = 1 (swGenericError). The reason was measured rather than guessed: GetUserPreferenceToggle(691) (swMultiCAD_Enable3DInterconnect) is TRUE. Under 3D Interconnect SOLIDWORKS does not translate a neutral file at all — it OPENS a STEP as a part holding a linked feature that points back at the source. So swx_import now reads that preference, reports it, and routes: OpenDoc6(path, swDocPART, …) when it is on, LoadFile4 when it is off. The reply says interconnect, linked and sourcePath, because a caller told only "imported" would be wrong about what deleting the source file does.

    The preference is never written — not set, and not flipped-and-restored: a crash between those two halves would leave the user's SOLIDWORKS configured differently than they left it. dissolve:true is offered and REFUSED (dissolve_not_measured) until the link-breaking call is verified on this install, with the refusal naming what to measure and the safe way round it; guessing an API that writes to a document is how pass 23's format claim would have gone if it had been a write. Import Diagnostics auto-run is also ON (toggle 291), so a faulty STEP raises a modal dialog mid-import: that is now classified, reported as stoppedByDialog, and never answered — the choice it offers changes the geometry the user gets. 360 tests.

  • v0.7.9: "Nothing" is not "omitted", and it had never worked. With 0.7.8's structured failure finally reaching the caller, the live import answered Type mismatch. (0x80020005) (argument index 1). rgvarg is packed reversed, so that argument is the THIRD parameter of LoadFile4(path, "r", importData, byref errors) — importData — which this bridge had passed as VT_EMPTY since v0.2.0. SOLIDWORKS wants an object there and will take a null one; it will not take "no value at all". The late-bound spelling of VBA's Nothing is a VT_DISPATCH variant with a NULL pdispVal, which is now what LoadFile4 gets — and what swx_com_eval's null literal builds, since its documented use is SelectByID2's callout, an [in] object parameter of exactly the same kind. There is still exactly ONE LoadFile4 call: no silent retry under the old encoding, because a guess nobody can see is worse than a failure somebody can read.

    The consequence worth stating plainly: LoadFile4 had probably never worked live, and the imports of 09-05 to 09-11 went through the LoadFile2 fallback — which today returns false, unexplained, and is the next thing to measure. None of this was findable until the HRESULT stopped being discarded one frame above where it was made. 353 tests.

  • v0.7.8: the sweep writes down what it decided — and the first autonomous close happened. LIVE, 2026-09-17: swx_close_all_safely took four documents to a recovery folder and closed every one of them, count 4 to 0, nobody asked to close anything; then swx_stop quit SOLIDWORKS on rung (a). ExitApp is NOT declined at zero documents on this edition — the long-held belief was a save-prompt stall, so the blocker was never the quit, it was the documents. Also measured: SaveAs3 to a neutral .sldrecover name answers "format not supported", so an unclassifiable document is now named .SLDPRT up front with namingFallback:"unknownKindNamedAsPart" recorded and the unknown-kind verification unchanged — the name is a label SOLIDWORKS forced, the floor is the check. The release's own work: every sweep now writes a manifest.json beside the files it wrote, rewritten atomically after each save and each close, carrying what no listing can re-derive — the original document's size, the kind GetType reported, and whether the extension was a guess. swx_recover_list reports the sweep's own verdicts from it (source:"manifest") and re-verifies only where no record exists (source:"reverified"), where a file that merely looks right is now reported as UNKNOWN rather than verified: a weaker check does not produce false negatives, it produces false positives, and verified:true for a file the sweep refused is read by the person whose sweep just failed.

    Also in 0.7.8: a failed import now shows the translator's own answer. Live, every STEP import began failing with LoadFile4 raising a COM exception, and the caller saw only "SOLIDWORKS could not import … (LoadFile4 opened nothing)" with the detail "SOLIDWORKS rejected 'LoadFile4'" — the HRESULT, the EXCEPINFO source and description and the failing argument index were already computed and then dropped. Every COM failure now carries them structured, and a failed import reports hostError.loadFile4 (message, detail, code, HRESULT) plus what LoadFile2 actually answered, so "refused" and "threw" are distinguishable. swx_com_eval stays read-only. 351 tests.

  • v0.7.7: the listing can tell a rescue from a husk. Files land in a sweep folder whether or not the sweep vouched for them — a refused save leaves one, a save that returns success can write something that is not a document, a full disk leaves a truncation — and in every one of those cases the DOCUMENT WAS LEFT OPEN, so the work is still in SOLIDWORKS rather than in the folder. swx_recover_list called all of them rescues, with sizes and instructions for opening them, under a headline saying every document closed was written here first. It now re-runs the same verdict the sweep used — it is a pure function of the file — and reports verified with the sweep's own reason per file. An unverified file is still listed and never deleted (those bytes are the only ones there are) but it is never called a rescue and never offered as something to open; its entry says the document was left open. The hint leads with "files marked verified:false are NOT complete documents". 344 tests.

  • v0.7.6: ask the evidence already in hand, and tell the user how to reopen what was rescued. When GetType fails, the document's OWN PATH is now consulted before its title — the path is the user's file as SOLIDWORKS named it, while a title often has no extension at all (Explorer hides known types by default). That narrows the neutral .sldrecover name to the only case with no extension anywhere: a pathless document whose type could not be read. Asking the path is not the circularity 0.7.5 removed — the path describes the document; the recovery file's name is chosen by the sweep FROM the kind, so reading a kind out of THAT can only repeat an assumption. If SOLIDWORKS refuses to write the neutral name (SaveAs3 dispatches its writer on the extension, and this bridge does not assume what it does with one it has never seen), the document is re-saved as .SLDPRT and the record says unknownKindNamedAsPart — the verification does NOT soften, because the kind is still unknown and the name is only a label. And swx_recover_list now gives every file its own howToOpen: a .sldrecover file says rename it and to what, rather than "reopen with swx_open", which is the one instruction that cannot work for it. 341 tests.

  • v0.7.5: the classifier can say "I do not know". Pass 26 found a data-loss path that only 0.7.4's per-kind floors made reachable, and it is circular: GetType failing on a document being closed under memory pressure — the condition the sweep exists for — became "part" via unwrap_or(1); the recovery file was then NAMED .SLDPRT from that; and the verification read the kind back out of the name it had just produced. A derived value cannot check the thing it was derived from, and a 30 KB truncated assembly cleared the 24 KB part floor (the smallest real assembly measured here is 95,492 bytes). The kind is now carried from the COM read all the way to the verdict: an unreadable type is UNKNOWN, an unknown gets a neutral .sldrecover name rather than a guessed one, and it is verified against the strictest floor plus — when the document had a file of its own — at least what that file weighed. Classification is by file EXTENSION only, so a recovery folder called …\Drawings\ no longer reclassifies every part under it as a drawing. A title is read as a title: Assem1 is an assembly, not a part. Refusing to close is the safe error, and the doc comment that argued the opposite is gone. 336 tests.

  • v0.7.4: sizes by kind, because the three kinds are not the same size. The 0.7.3 estimator costed a document it could not measure at twice the largest it could, which is not monotone: one measurable 29 KB part beside a pathless assembly LOWERED that assembly's assumed cost from 1 MB to 58 KB — learning something small about a session made the guess about the rest of it smaller, and that same number gates the check before every save. An unknown document now costs at least a floor set by its kind (part 256 KB, assembly 8 MB, drawing 4 MB, each above the measured median), so a measured neighbour can never lower it. The recovery floor splits by kind too — part 24 KB, assembly 80 KB, drawing 256 KB, each just under the smallest real document of its kind measured here (29,130 / 95,492 / 319,803) — because one floor for all three has to be the lowest of them, which would let an assembly stop at a third of its size and still pass. 330 tests.

  • v0.7.3: it refuses to start a sweep it cannot fit. Pass 24 found the gap the 0.7.2 checks structurally cannot close: the container marker lives at offset 4, so ANY prefix of a real document carries it, and a write that reaches 20 KB of a 500 KB assembly clears the 16 KB floor too. Marker and floor catch "empty" and "not a SOLIDWORKS file"; neither catches the right file, cut short, and the format has no constant tail and no length field to check against (measured). The remaining defence is the decision to begin: the sweep now estimates what the session will write, refuses to start when the recovery volume holds less than twice that (insufficient_headroom, naming free bytes, needed bytes and the shortfall, nothing written, nothing closed), and asks again before every save because it is itself one of the things filling the disk. headroomBytesBefore and estimatedWriteBytes are reported always. A refusal no longer leaves an empty recovery folder behind for swx_recover_list to present as a rescue. 329 tests.

  • v0.7.2: the format check, measured. Pass 23 asked for the recovery file to be verified against the OLE compound-document signature D0 CF 11 E0 A1 B1 1A E1 — the answer every reference gives for what a SLDPRT is. Measured against 166 real documents from three hardware repositories on this machine: zero of them begin with it, and it appears nowhere in the first 4 MB of any of them; all 166 carry 00 00 00 04 at offset 4. Shipping the signature as specified would have rejected every document on a modern install — failing closed, so nothing lost, and the sweep would have preserved everything and closed nothing for ever, which is the feature. The check now accepts either container, fails closed on anything else, and is pinned by real captured headers; the byte floor rises from 1 KB to 16 KB, below the smallest real document observed (a 29,130-byte part) and far above what a failed write leaves. The dialog census also moved from once per batch to before every save and before every close, because a save can raise a prompt of its own and a per-batch check could not see it until the closes had already happened; the host now does one document per call and batch is only a round-trip bound. 324 tests.

  • v0.7.1: the close sweep, adversary-hardened. Four things stood between "nothing is lost" and a claim anyone could check. The recovery write is now verified by SIZE through a constant both backends share, and the size is published as recoveryBytes — a zero-byte file is what a full disk produces and exists() passes it, which would have authorised closing a document whose only copy was in memory. An UNREADABLE window census is now its own outcome: the saves proceed, no document is closed, and the reply says stoppedBy:"dialogCensusUnknown" rather than implying a dialog check that never happened. The SaveAs3 re-point is stated in the code and pinned by a test that reads the user's original bytes back byte-for-byte. And the simulator now models what makes the close ORDER matter — a component cannot close while its assembly holds it open, and a refusal is written down — so a sweep that asks in the wrong order fails the test instead of quietly succeeding on the retry. Each of the four is mutation-verified: disable the rule and exactly one test fails. 316 tests.

  • v0.7.0: the bridge closes its own documents now. Drew: "I despise every time you ask for help closing. You MUST find/build a way to safely close the docs/program." swx_shutdown {reason} saves every open document into a recovery folder, closes them all, and quits — no write opt-in, no per-document confirmation, no environment flag. The authorization is to CLOSE, not to lose anything, so what replaces the consent question is a sequence: nothing closes before it is on disk, verified per document, and anything that could not be written is deliberately left open and named. A document the user had saved and then edited is copied into recovery rather than saved over, so their file is never modified and their unsaved changes survive too. A save prompt cannot appear because nothing is unsaved by the time a close happens — and if a dialog appears anyway, the sweep stops and reports it rather than answering it. swx_close_all_safely is the sweep without the quit, swx_recover_list finds what was preserved, and swx_stop {userAuthorized:true} may now quit a session this bridge did not launch. 308 tests.

  • v0.6.9: two things the suite could not see. The quit ladder re-measures every precondition before every rung, and the launch-claim check is part of that sweep — but nothing could demonstrate it, because the simulator's attached pid was fixed for a whole run, so the FRONT GATE would always be what caught a mismatch and the per-rung call site could have been deleted in silence. mock-com-pid now hands out a different pid per read, and a test makes the session valid at rung (a) and different by rung (b); neutering ONLY the per-rung call site fails that test and nothing else. And tests/wiring.rs proved "has a caller", not "is called where the bridge decides something" — callers must now be in a named set of decision-making files, with the simulator deliberately not among them, so a guard whose only caller is the mock reads as unwired. The file also now states what it does NOT prove: that a call exists is not that it is reachable, in the right place, or passed the right arguments. 303 tests.

  • v0.6.8: unit-tested is not wired. identity_matches_attribution shipped in 0.6.7 with ZERO production callers — the fifth guard in this project to do that, and the third about attribution. The commit message said it was wired into swx_stop; the edit that would have wired it was in a script that aborted partway through, and nobody checked the file. Every test was green, because every test tested the function. It is now called from swx_stop (at the front gate AND in the per-rung re-sweep), swx_close_all_documents and swx_reset_workspace, each with a test that drives the VERB and asserts identity_mismatch in its response — mutation-checked by deleting the CALL SITE, leaving the function and its unit tests untouched. And tests/wiring.rs now lists every guard in the bridge and fails if any has no production caller, so retiring one is a decision somebody makes rather than a silent edit. The audit that built that list found exactly one other candidate, and it was a false alarm: hwnd_still_owned's only caller lives in #[cfg(windows)] code. 301 tests.

  • v0.6.7: ordering is not equality — a consent-affecting bug I shipped in 0.6.6. That release fixed the walk to try every sldworks.exe, and used the COM-attached pid only to ORDER the candidates, taking the first that descended. With two SOLIDWORKS running it could therefore prove lineage for our launcher's child while COM was attached to a DIFFERENT process, and then label the session attributed:true. Everything downstream reads that label — the quit gate's bridge-launched proof, and a quit ladder that builds its Identity from the COM pid — so the bridge would have been willing to END a session nothing had proved was ours. Re-attribution now succeeds ONLY when the process that descends IS the attached one (pid and creation time), refusing attached_pid_not_ours otherwise; the evidence names the proved pid, its creation time, the parent it came from and that it equals the attached pid; the ledger lists every candidate with its own clause instead of "the last one stands"; and the verbs that END a session now refuse identity_mismatch if the launch's recorded claim is about a different process than the one COM answers for. 296 tests.

  • v0.6.6: the lineage walk took the first SOLIDWORKS in the census, and it had no parent. Re-attribution had been unreachable since 0.6.0 and three passes failed to find out why from outside, because it refused silently. So the diagnostic came first: every return None on that path now names its clause, reported as reattributionRefusedBecause and ledgered. The positive case then answered in one word — no_parent_recorded. census.iter().find(is SLDWORKS) took whichever process the census listed first; that one had no ppid, the walk ended there, and a second sldworks.exe further down the list — descended from our own launcher — was never examined. It is the same defect host_app_health was rewritten for in 0.4.x ("EVERY sldworks.exe, not just the first"), reappearing in a walk written two years later. Now every sldworks.exe is tried, the one COM is attached to first. Also: the mock's status consults the simulator the way a real GetActiveObject consults the machine, so a session that comes up while nobody is calling swx_start — a user finishing a sign-in, which is the entire case this feature exists for — is visible to swx_status alone. 292 tests.

  • v0.6.5: a real child is created while its parent runs. The lineage window was a flat 30 seconds — three census intervals of invented slack, and slack in an identity rule is the space a recycled pid lives in. It is now the census interval itself, which is the only reason the window is not zero: our knowledge of "while it ran" is a poll, so a child born in the gap between two censuses was still born while the parent ran. The simulator can finally express PID REUSE (census-overrides.json retires a pid and reissues it with a new creation time), which nothing before it could pose. And re-attribution being possible only for an ATTRIBUTED route is now written down as a rule and pinned: explorer and cmdStart cannot say what they started, so there is no identity to walk a parent back to, and a session that came up after one of them is not claimed however plausible it looks. 290 tests.

  • v0.6.4: the same mistake one route further down, and a window around lineage. 0.6.3 stopped the bridge calling its ATTRIBUTED launcher foreign; pass 16 found it still doing so for the routes that cannot name what they started. explorer and cmdStart have no attribution root, so their root and descendant sets are empty and their own launcher processes matched every foreignness clause — on a machine whose only launcher processes were the ones the bridge had just started. Foreignness now needs POSITIVE evidence (a launcher that PREDATES our invoke); one that appeared after it, while a route that cannot name its root was running, is recorded as possiblyOurs — excluded from the foreign set and, as always, from the kill path, because "possibly ours" is a reason not to accuse and not to terminate alike. And lineage got the window it was missing: a dead parent is still acceptable (the launcher always exits), but it must have been SEEN ALIVE with that exact pid and creation time during the attempt, and the child must have been created inside that observed lifetime plus 30s — otherwise a user's launcher that recycled our dead pid would have its SOLIDWORKS adopted as ours, with nothing alive left to contradict it. 285 tests.

  • v0.6.3: the bridge called its own launcher a stranger. Foreignness was judged BEFORE the chain was recorded in the same census pass, so on the first census after an invoke the recorded chain was still empty and only the attribution ROOT was excluded — our own SWXDesktopLauncher, the root's own child, matched "terminable, not ours, not baseline, not the root" and latched foreignChainSeen on every ordinary launch. That is the foreignChainSeen:true seen live on 09-11, and it made sign-in recovery INERT: re-attribution bails on a foreign chain before it ever reaches the lineage proof. The chain is now recorded first and foreignness judged against the updated set plus every descendant of our own root. Three more things fell out of fixing it: a route ending sldworksUpNotAttached recorded no row at all, so the most important outcome for sign-in recovery had no evidence to point at; the log-authorship lineage proof is GONE, because "our launcher ran" is not the claim "this SOLIDWORKS came from it" (a user starting SOLIDWORKS from the Start Menu while our chain ran satisfied it); and re-attribution never checked that COM was actually attached, so it would relabel a session that was merely UP. 283 tests.

  • v0.6.2: two one-liners that were not one-liners. Pass 14 found the lineage proofs matching a bare pid NUMBER: Windows never validates a parent id, and our launcher chain always exits once SOLIDWORKS is up — so a process whose parent had merely REUSED that number would have satisfied the walk. Lineage now carries (pid, creation time) and matches through ProcInfo::same_process, the same identity rule as every other claim in the crate, and the parent must still be alive to be matched at all. And the case-insensitive uniqueness guard was real but incomplete: there are TWO verbs that close a pathless document BY TITLE — the registry drain and swx_reset_workspace — and the rule was in one of them. It is now one function used by both, with a test for each that puts the bridge's untitled Part1.SLDPRT beside the user's saved part1.SLDPRT and requires both verbs to refuse and say so. 282 tests.

  • v0.6.1: three hardenings from adversary pass 13, which found no defects. The registry drain judged a title-only document unique with a case-SENSITIVE == and then closed it by name — and CloseDoc resolves case-insensitively on Windows, so the bridge's untitled Part1.SLDPRT beside a user's saved part1.SLDPRT looked unique to the guard and ambiguous to the act. A guard must ask the question the way the act will answer it. An unlistable open-document set is now "not unique" rather than silently skipped (ownedUnsavedSweepBlind, and done cannot be true). Re-attribution after a sign-in now requires POSITIVE LINEAGE — a parent walk from sldworks.exe that reaches our own chain, or a launcher log a thread of our chain wrote — because "no foreign chain was seen" is the absence of an observation, and a chain that started and finished between two censuses was never seen at all. Adopting a document after a timed-out import now requires all four of: unsaved, title stem matches, not open before the call, and the session gained EXACTLY one document; anything less adopts nothing and says what it could not establish. 279 tests.

  • v0.6.0: a sign-in wait is not a stall, and a title is not an identity. The first live gate run proved the attributable world — shellExecute handed back a real handle for the Platform shortcut (CATSTART pid 131272) and that chain reached Run Begin — and found the launch machine killing its OWN chain while Drew was at the 3DEXPERIENCE ID sign-in, because the sldworks deadline fired on a wait that was not a stall. The sign-in window is now recognised by class and owner pid, every deadline pauses while it is up (an unreadable window census pauses too), nothing is killed or escalated, the user is toasted, and a session that attaches afterwards is re-attributed instead of being recorded as somebody else's. Separately: a fresh import has NO path live, so two imports of one file shared a title and neither could be named by any verb — every bridge-created document is now given a file of its own at birth (documentIdentity, keepUnsaved:true to opt out), the drain recognises bridge-owned unsaved imports by registry and names the ones it cannot address, and a mutating call that fails AFTER creating a document still owns it. 275 tests.

  • v0.5.9: the write gate's own unanswerable question. The destination test was Path::exists(), which is false both for "nothing is there" and for "I could not look" — a parent it cannot traverse, a UNC share it has no token for, a device not ready, an over-length path — and the gate read the second as "free" and allowed the write with no opt-in. The com host is a child of Adom Bridge while SOLIDWORKS holds the user's network token, so \\server\share\part.sldprt the bridge cannot stat is one SOLIDWORKS overwrites. Now symlink_metadata gives three answers — occupied, free, or unverifiable, which REFUSES (destination_unverifiable) before the opt-in is even consulted, in BOTH enforcement layers, from one shared function, tested in both. Also: the com host now names its version in the ready handshake and a server/host skew is an explicit boot error, because closeOutcomeUnknown read with unwrap_or(false) turned a field an older host never sends into "nobody needs to look". 270 tests.

  • v0.5.8: an unreadable count is not "nothing happened". The drain's discriminator had two arms and needed three: when a close reports success while the document is still open, a count that did not move means the call did nothing (close_by_path_unsupported) and a count that dropped means something else was closed (wrong_document_closed) — but a count that CANNOT BE READ means neither, and it can become unreadable because something large just changed. It is now close_outcome_unknown / outcome:"undetermined": the drain stops and reports the path and the pre-close count with "a human must check whether this path, or another document, closed". The mock read its count from a Vec length, so that arm had no fixture at all; every count now goes through one seam that mock-doccount-none can make unreadable. tools/drain-gate.py also stops overclaiming: a PARTIAL banner when the collision half was not exercised, the twin save attempted in the DEFAULT posture first with the result recorded as a finding (a bridge-created document written to a new path in a scratch dir must pass the choke-point rule — pinned in the suite, and a live refusal would be a bridge bug worth naming), and cleanup that deletes only the two files it wrote rather than recursing a directory whose contents it did not write.

  • v0.5.7: the close key is right; the window it reasoned over was not. done never consulted WalkStop::complete(), so a walk that stopped at its scan cap or on an invalidated iteration reported a finished drain with temporaries still open past the window (measured: 55 of 60 scanned, a temp at #60 still open, done:true). done now requires reaching the end AND documentsScanned == documentCountBefore; walkStop is published; and the collision claim is scoped to what it was checked over (titleCollisionsCheckedOver, and the hint says so). The mock selected victims across the whole session while reporting a 55-document scan — it is window-bound now, which is what makes any of those properties testable. And the premise the whole close rests on — that CloseDoc accepts a FULL PATH, which its help text does not promise — is now discriminated rather than assumed: a close that returns success and moves no count is a CAPABILITY failure (close_by_path_unsupported), the drain stops, and it never falls back to a title; only a count that DROPPED while the victim is still open is wrong_document_closed. tools/drain-gate.py proves the premise live on documents the bridge makes itself.

  • v0.5.6: a title is not an identity — and the drain's CLOSE was the one place that rule was missing. Victims were chosen by PATH and then closed with CloseDoc(title), which resolves by name and does not prompt: on Drew's live session, leaked temps titled First.step-1.SLDPRT sit beside his SAVED …\validation\First.step-1.SLDPRT, so SOLIDWORKS would have chosen which one died while the response recorded the temp as closed either way. The 254 green tests said nothing about it because the mock removed documents by a unique internal key. Now: the victim is closed by its full path; any victim whose title another OPEN document shares is refused and reported in titleCollisions with every path that shares it; every close is verified with GetOpenDocumentByName, and a close that reports success while the document is still open stops the drain and fails loudly as wrong_document_closed. The mock models CloseDoc's by-name resolution, so a close-by-title now fails the suite. A walk that ended early (a GetNext on an invalidated iteration) is no longer reported as complete: walkComplete / walkStopped are measured and done requires reaching the end.

  • v0.5.5: the degrade path was itself an unbounded marshal. Found on the live 2144-document session the hour 0.5.4 was deployed: swx_docs still timed out at 30 s and the com host was killed, while GetDocumentCount answered in 0.7 s. Above the cap the code called GetDocuments — one call that marshals every handle — and then GetPathName on all 2144 of them to fill a field named importTempsInSample. The drain had the same shape, so it would have wedged on the very session it exists to drain. Now everything walks: GetFirstDocument()/GetNext(), one document at a time, bounded by a count and a time box; the sample costs the sample; the drain scans a bounded window and reports documentsScanned; documents() refuses above the cap rather than letting a caller find the wedge; close_all_documents still closes a flooded session but reports counts with null lists instead of empty ones. And the mock can now be SLOW (SWX_MOCK_DOC_LATENCY_MS, SWX_MOCK_OPEN_DOCS), because the reason every test passed is that its enumeration was instant — the regression test holds the live shape: 2144 documents, cap 200, 15 ms each, swx_docs in under 3 s. Ecosystem row 37 is marked mock-proven; failed live; re-proof pending until it is re-run on Drew's session. 254 tests.

  • v0.5.4: adversary round 8 — the anchor was right, its failure was silent. The drain compares a path SOLIDWORKS reported against a %TEMP% this process resolved, and the two can legitimately differ (ab may run under a service profile or a redirected TEMP, and the leaked temps were written by an earlier, dead bridge process). On a mismatch 0.5.3 matched nothing and reported closed:0, done:true with "Every open STEP/IGES import temporary was closed" — a confident wrong claim about the user's session, and the step an operator would have read as finished before the quit gate refused on open documents. Nothing was ever wrongly CLOSED; the report was wrong. Now: tempRoot is in the response, documents carrying the launcher's …\swx<pid>\IC~~\<file> tail under an unresolved directory are counted as importTempsElsewhere with a sample path, and a matched-nothing drain reports done:false with a hint saying what to point it at. The candidate roots are this process's %TEMP% and the interactive user's HKCU\Environment\TEMP expanded — a document under either qualifies, no root still refuses — because the user's registry value is the least invasive way to learn the temp directory of the session SOLIDWORKS is in. The 8.3 rule is now exact rather than merely tight: the mangled form needs its six-character head, the COLLISION form (DREW~1 for drew, which is what a domain-joined machine really produces beside drew.ADOM) needs the whole name, and a loose prefix is rejected.

  • v0.5.3: adversary round 7 — the once-swept precondition, the one-sample exit, and the anchor that undid itself. The drain's %TEMP% anchor was OR-ed with a shape test, which made it advisory: any path whose tail read …\AppData\Local\Temp\swx<digits>\IC~~\<file> qualified for a no-prompt close wherever it lived — a restored profile backup on another volume, another user's profile — and when %TEMP% was unreadable the shape alone decided. The resolved temp dir is now the whole location test (8.3-aware on both sides), and with no resolved root the drain refuses (temp_root_unknown) instead of reporting that it found nothing. In the ladder: the preconditions were swept ONCE before the loop while the code promised "before every rung", so a document opened while rung (a) settled was still open when rung (b) fired — they are now re-swept per rung, identity included. The exit was judged from ONE census read 1.5 s after the rung, so a real ExitApp that returns before the process goes read as "declined" and the ladder ESCALATED into an exiting application — it now polls up to the rung's own budget, and the simulator can model a slow exit. The dialog census could not see a 3DEXPERIENCE sign-in modal, because a web view's window belongs to a swCefSubProc.exe child; it now covers those pids, by parentage. The open-document count is cross-checked against the enumeration. Rungs (c)/(d) re-verify that the hwnd still belongs to the same process before sending anything. 248 tests.

  • v0.5.2: the graceful QUIT ladder, and the 0.5.1 adversary round. swx_stop v1 called ExitApp, asked the COM running-object table whether SOLIDWORKS was still there, and reported a DECLINE as success:true. v2 is a measured ladder — rung (a) ExitApp through rung (f) the Restart Manager without RmForceShutdown, pinned at (a) — under three laws: never a kill (there is no terminate call in the stop path, and a test pins its absence), never answer a dialog (a new modal stops the ladder permanently and is reported, not clicked), and nothing is disowned until the process is verifiably gone (v1 cleared its registries first, so a declined quit left the bridge unable to save its own documents). Seven preconditions are re-measured per call and an UNKNOWN in any of them refuses; force waives the UNSAVED question only, never the open-document one. Every COM reference is released before and after each rung, and outstanding refs are named in the report, because a "decline" measured while we still held a reference is not evidence. Success is this pid with this creation time absent from the census, never a return value. Also folded in: the 0.5.1 adversary round (a drain that could close a user's saved file, a silent no-op sweep, ownership rows that could be reassigned to another document). tools/quit-gate.py is written; no rung has ever run against a real SOLIDWORKS, so docs/QUIT-INVESTIGATION.md §5.11 and the ecosystem table both say unproven live.

  • v0.5.1: the 0.5.0 adversary round, plus busy-vs-dead. is_import_temp proved the wrong thing — two tokens anywhere in the path, in any order — so a user's own saved file under folders they had named Temp\swx-archive\…\IC~~\ qualified and would have been closed with no save prompt. It is now a segment walk requiring the launcher's contiguous Temp\swx<pid>\…\IC~~\ layout. An unreadable document count is now UNKNOWN-and-truncated instead of falling through to the 30s enumeration it was meant to prevent, and a verb that cannot resolve its target says document_list_unavailable rather than "no such document". Making the mock report the realistic temp path (as GetPathName does) immediately exposed three more real defects it had hidden — including the bridge refusing to close the document it had just imported. And, from live evidence: hung now carries verdict:"busy"|"dead"|"unknown" with cpuActiveSeconds/memoryDeltaMB, because a session at 17.9 GB was reported hung for five minutes while burning 70% of a core, and it recovered. 202 tests.

  • v0.5.0: import / document-lifecycle robustness. Root-caused a live 1342-document / 8 GB leak — the same two STEP fixtures imported over and over and never closed, the real root of the crate-build memory exhaustion. Enumeration now DEGRADES instead of wedging: swx_docs/app_state read GetDocumentCount first and, above a cap, return {count, truncated, sample} rather than marshalling thousands of handles for 30s and getting the com host killed. New swx_close_import_temps drains the leaked …\Temp\swx<pid>\IC~~\… conversion temps in the DEFAULT posture (the path is the ownership proof, and survives the pid change that made the leak un-ownable); swx_import asserts a net +1 and says the document stays open until closed. swx_import's contract is unchanged. Ecosystem row 43 — we are alone. 191 tests.

  • v0.4.4: evidence bookkeeping. The fourth adversary pass again found nothing consent-affecting — the process-side law has survived four rounds — and closed the last version of the same mistake: "our chain existed before this log" is true of every log written after our chain started, and the newest- first listing meant a second launch's log was preferentially read. A log is now chosen by AUTHORSHIP (the launcher stamps its writing thread's id on every line, and thread_owner resolves it to a process in our own chain), or by being the sole candidate with no foreign chain alive, or not at all. Plus: every per-route fact resets per route (notConfirmed was unreachable after route 1), RouteTry carries its own evidence so the gate's per-route table stops printing one row's value on every row, and the gate itself now exits 3 on an unreadable probe instead of reporting a pass it could not see. 186 tests.

  • v0.4.3: the law applied to evidence. A third adversary pass found no consent-affecting defect — it could not make the bridge terminate a process it did not start — but the bridge still adopted a stranger's EVIDENCE: a launcher log was claimed on "newer than the baseline" alone, so a launch the user started mid-watch supplied our Run Begin and taught the bridge that a route which started nothing was the one that works. A log is now ours only when our own attributed chain existed to have written it. Plus: a handle-proven root seeds the lineage walk even when the census cannot see it (a shim or wrapper would otherwise leave a real chain running under a "clean recovery" report), "never confirmed the root" became its own stop condition, and a shared temp filename that raced across HTTP worker threads was fixed. 181 tests.

  • v0.4.2: the attribution law. A second adversary pass proved 0.4.1 still terminated a user-started launcher chain, because "the first CATSTART after our invoke" was a guess dressed as an inference. The rule is now no handle, no kill: a process is ended only when the invoke itself handed back its root (ShellExecuteExW's handle, Start-Process -PassThru's pid) and it descends from that root; the two routes that cannot say what they started never clean up, and a route that leaves something unaccounted-for stops the machine. Plus: a failed launcher-log listing is UNKNOWN rather than "any log is ours" (it would have fabricated the gate's central measurement), is_newer_than consults both clocks and distrusts a stamp the filesystem contradicts, timed_out is carried explicitly instead of sniffed out of an error message, and a corrupt ownership registry falls back to its previous generation. 175 tests.

  • v0.4.1: the adversary round. 14 failing tests written against 0.4.0's own claims, all now passing and part of the suite. The kill list is scoped by IDENTITY (baseline census + parent-pid lineage + a creation-time re-check on the open handle) instead of by clock, so a launch the user starts mid-flight is never swept up; documents() no longer believes an unreadable count; exhaustion needs the one signal that proves it rather than a memory threshold that flagged a healthy 13 GB session; hung needs a real COM timeout and not this bridge's own busy com host; a stale attempt from a dead bridge is marked abandoned; a cleanup that could not run says so and stops the machine; crash-reporter recovery ships DISABLED until the gate has seen a real UIA reply; and the registry and launch state are written atomically. 162 tests.

  • v0.4.0: launch resilience. A launch state machine with stall detection, route fallback and a two-image kill allowlist; in-flight idempotence (a swx_start during a launch starts nothing); crash-reporter recovery, clicked once and by button name; honest host_app_hung / host_app_exhausted / host_app_com_unreachable reporting read WITHOUT COM; and bug #5 closed — the ownership registry is persisted and an unanswerable probe no longer disowns everything the bridge created.

  • v0.1: full verb surface in Python + a PowerShell COM sidecar, mock-tested.

  • v0.2: single Rust binary — 23 verbs, COM in a killable child process, crash recovery, a suite that drives the real binary.

  • v0.3.1 (this): the launch answer — SOLIDWORKS Connected starts through the Platform shortcut, so host_app_launch_blocked now means only "no shortcut found"; swx_run_macro scaffolding; and the write opt-in made openable on every path it guards (three live-proven cases where the human authorized an action in full and the bridge refused anyway — an escape hatch nobody can open is not a safety property).

  • v0.3: 60 verbs at Fusion-360-bridge parity — document creation, a modelling DSL, geometry read-back, parameters, per-format exports, window and dialog etiquette, an action ledger and measured verb timings. First live shakedown against a real SOLIDWORKS; four v0.2 COM bugs diagnosed with live evidence and fixed on reasoning (the fixes themselves are unverified — see the parity report). Writes made deny-by-default after five rounds of review.

  • v0.4 candidates: drawing (.slddrw) BOM-table extraction and DXF/DWG/PDF from drawings, per-component mass properties for assemblies, configuration management, a live screen feed, and — the big one — 3DEXPERIENCE 3DSpace access, which needs its own OAuth story.