SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
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 sameBackendtrait — 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 againstswx_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_filesruns 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 inAdom 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.
Commit the release (version bump, tests, docs).
tools/release.sh --canarybuilds the zip and publishes it as the release<version>-rc<N>. It also writes the page filemanifests/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.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 declaredupdateManifestUrlfirst (adom-bridge help bridge_check_updates). Every zip'sbridge.jsondeclares the MAIN manifest there, and the canary file says the same.Run
python3 tools/cswa-check.py --target adom_drew2 --report autothere. The report lands intools/cswa/reports/, and it records which build ran inbridge:- the installed exe's sha256, read back through ab's
read_file; - the manifest it was installed from, and that zip's sha256.
- the installed exe's sha256, read back through ab's
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 --publishrefuses without--require-live <report>, before anything is built.--docs-onlyskips the gate, and its changelog says so. It is allowed only whentools/check-docs-only.pyfinds nothing changed insrc/orresources/(untracked files included) and nothing in Cargo.toml or Cargo.lock beyond the version.tools/check-live-report.pynow also needs EVERY shipped part to have run: a run narrowed with--onlyis 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 --canarypublishes the build as the release candidate<version>-rc<N>, withmanifests/canary.jsonnaming it; it is installed by hand, on the test machine only.--require-live <report> --publishthen promotes those exact bytes, with no rebuild. It does so only when the report says it ran that exe (bridge.exeSha256, whichcswa-checkreads back from the installed copy) and no code changed since the canary.cswa-check --report autonames 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.regionsAtMmonextrude/cut: SOLIDWORKS' Selected Contours, done as its own example does it (EnableContourSelection, thenSelectByID2 "SKETCHREGION"at mark 4).Tool Block Step 4 runs, two ways.
cswa-08uses Offset Entities, regions and twooffsetFromSurfacecuts; it runs live only.cswa-08bdraws 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-09tocswa-12build 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,upToSurfaceandoffsetFromSurface;- draft on a boss and on a cut.
Each key is derived analytically. Each spec's
derivationre-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/FeatureCut4takeDdir1; 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
swEndCondUpToSelection10 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).swEndCondUpToSurface4, the id the macro recorder writes, is what the bridge sends now;offsetFromSurface5 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 fromIExtrudeFeatureData2), 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.
- The draft flag was inverted.
A
derivedAppendmay no longer shadow a name its base derives. In the simulator, Step 4'stpxhad 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
ready4 s after attach. Aswx_new_document11 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.evidencelists each signal with its age, andreadiness.whysays 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_openandswx_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 ishost_app_died(with the pid, the ledger line and the crash markers) ordocument_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:
helperSignInpauses 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.exefrom this launch exists. It reads the census before cleaning up and watches for 15 s before any retry. It ledgerslaunch_retry_declinedand 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 byswx_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_stopafter it wasstop_blocked_by_dialog; - the census called it
kind:"unknown",bridgeCaused:false, andswx_stateonce reportedblockingDialogs: []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 recordsdialog_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.
- Replies carry
- 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 everyswx_statecall,screenshot:falseincluded (that path used to skip the census and say[]). ab's list is only the fallback. No visible modal is dropped: the quit preconditions reportmoreBlockingDialogs. 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 ledgersdialog_cancelledwith the attribution. Anything with text typed, or of unknown origin, is the user's. - Quit rung (b) reads
swCommands_App_Exitfrom the installed interop and does not sendRunCommandunless 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:falseuntil the coordinator's run).- Sketch geometry:
line,centerline,polyline,arc(centre and angles),arc3,tangentArc,slot,polygon,rect.cornerRadiusMm,sketchFillet,sketchChamfer, andconstruction:trueon 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).mirrorof 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_eread from the interop).hole: plain, counterbore, or countersink (a cut drafted at half its angle), as cuts on the face's plane. End conditionsupToNext(11),upToSurface(UpToSelection 10, the face at mark 1),offsetFromSurface(5), anddraftDeg. A plane offset from a face.set_material {densityKgM3}. swx_mass_propertiesaddsmassG,volumeMm3,surfaceAreaMm2,centerOfMassMm,densityKgM3andmaterialName.- 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 andextends. Step 4 is skipped: it needsoffsetEntities.tools/cswa-check.pyruns 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.
- Sketch geometry:
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 asPart14-….SLDPRT, and with Windows' "Hide extensions for known file types" on (the Windows default) SOLIDWORKS titles itPart14-…. 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
documentsLeftOpeninstead of leaving a part silently. swx_new_documentreports 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_selftestcloses what a cut-off run left.swx_status.orphanedSelftestDocumentsreports them, and closes nothing (a run may be in progress). Two self-tests cannot run at once (selftest_running). swx_selftestreplies (and its catalog entry) lead with_timeoutHint: call it withresponseMode:"stream"or"job".- 2021 is tested.
compat::OP_FACTScarries the community measurement (by:"community") for the 32 variants the run measured, soproven.liveis 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.
- Titles match extension-insensitively everywhere (
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_workspacecalls 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 towaitForIdleSeconds, default 120), then refuse withuser_presenthaving 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_workspacereportsleftActive. It is refused only when no document qualifies, and each close is ledgered withuserPresent: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_scriptreportsdocumentsLeftOpenwithwhy:"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.
- 17
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 singleDeleteSelection2returned true and left a top-level sketch that an earlier check had left unconsumed (a cut that removed nothing keeps its sketch). Now:clear_partwalks the tree again after each delete and deletes what is left. It selects a feature byIFeature::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 reportspassesanddeleted. 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
reuseFallbackon the row. It counts the part indocumentsCreated, 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.
- nothing was throttled, and the launch was attributed by
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_respawnfreed 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_documentand scripts are all visible. A script that would model a hidden part is refused up front withdocument_hidden. - One visible reused document per battery. The self-test uses it,
reports
documentsCreated, and ends by respawning the COM host (comRespawn.freedMb). AreuseDocumentthat 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_partis 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).
GetDocumentCountwas 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:falseonswx_new_documentand on the script'snew_part/new_assembly. It wraps NewDocument inISldWorks::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_partop deletes every feature after the origin, usingDeleteSelection2with children and absorbed features, and only in a part the bridge created.swx_run_script {"reuseDocument": …}turns a script's leadingnew_partintoclear_part. The self-test runs every check it can in one hidden part, and reportsdocumentsCreatedandreusedDocumentper 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 withswDontRebuildActiveDoc. It passed 0, which is the promptingswUserDecision.- Whether hiding or reuse stops the growth is unmeasured.
623 tests.
- Hidden documents.
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.memoryreads the commit charge (GlobalMemoryStatusEx), SOLIDWORKS's private bytes and handles, and the top five processes, with no PowerShell. It reportspressureas ok, high or critical against a 6 GB session budget (SWX_SW_MEMORY_BUDGET_MB). Every reply's_hintnames the largest holders while pressure is not ok.- At critical, verbs that create or load geometry are refused with
low_commit_memoryand change nothing. The refusal gives the numbers, the holders and the one safe remedy.ignoreMemoryPressure:truewith 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 withstoppedFor:"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, andswx_selftest {"level":"write"}passed 43 with none failed (docs/LIVE-RESULTS.md). Every live selection used the first rung, the typedMultiSelect2, and it stays first. Each step'sprovenis now{"live": …, "measuredOn": [{"solidworks": 2026, "major": 34, …}]}in place offalse.liveis true only when every variant the step ran was measured on the release it ran on. The variants come from one table incompat.rs, whichswx_describeandswx_selftestalso report asliveMeasured. Variants no live check has built say so underunmeasured: 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 itsroutelike 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 whenGetSelectedObjectCount2grows by what was asked:MultiSelect2with a typedVT_DISPATCHarray;IEntity::Select4, per entity, through the object'sIEntityinterface (the IID is read from the installed interop, and the bridge checks the interface is dual first);SelectByRaywith the bridge's own ray, from 1 mm outside the point back onto it, so it does not depend on the view;- last,
SelectByID2at the point, which does depend on the view.
Each step reports the rung that worked as
selectedVia, andviewDependent:truewhen rung 4 was needed. The simulator now models the measured install, and a fault per rung shows the ladder falling through.ISimpleFilletFeatureData2.Edgesgets 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 (
GetClosestPointOnover 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 (
IRefPlanehas 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;
FeatureFillet3falls back toCreateFeature.
Also fixed:
ActivateDoc3was 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 astools/dsl-live-check.py. 587 tests.- faces and edges are picked by GEOMETRY (
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_selftestcountedswx_docsandswx_get_preferencesas 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.classicYearssaid[2026]on the 3DEXPERIENCE 2026 install.registryYearsnow carries the raw registry years, andclassicYearsonly the years with no 3DEXPERIENCE entry or install path for that release, which is[]on that machine. The edition decision is unchanged. 576 tests.- every check that needs SOLIDWORKS is skipped, with the reason, and the
summary says
v0.10.0: issue #1, the modelling DSL Michael asked for, and SOLIDWORKS 2021 as a supported release.
- Sketches. One
sketchholds 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"andcenterMmplace rectangles. - Extrude and cut. Both take
reverse,direction:"both"andmidPlane. 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_patternandset_appearance.swx_inspect_bodiesreads 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.jsonbuilds 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.mdaudits all 149 COM members the bridge calls against help.solidworks.com. None is newer than 2017, sobridge.jsonnow declares>=2021(supported) and 2026 (tested). - Version facts.
swx_status.solidworkscarriesmajorVersionandversionSupport. Version-gated calls refuseneeds_solidworks_versionby name. - Classic installs. Detection recognises a classic desktop install from
its
SOLIDWORKS <year>registry key, andSWX_PROGIDpicks one release when several are installed. swx_selftest. It returns one JSON of per-verb pass/fail on any install.readchanges nothing;writeuses 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.
- Sketches. One
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_bodiespassed0.0toIBody2::GetMassProperties, whose argument is the DENSITY (help.solidworks.com, Availability 2001Plus). ItsmassKgcame back as volume x 1, about 1000x small. Each body's mass now uses the density of anIMassPropertyrestricted to that body (AddBodies), or the part's if that is unavailable. The reply carriesdensityKgM3anddensitySource, and the mass matchesswx_mass_properties. An unreadable density givesmassKg: null.swx_run_scriptdecidesbridgeCreatedDocumentwith 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.launchstill 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 reportsstage:"ended",endedWhyand honest advice. The skills also say wheresignInWindowlives: at the top level ofswx_status, besidewaitingOn.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_revealto a bridge ("requires a VERIFIED local transport … origin "bridge""). That is ab's policy, and the bridge does not work around it.swx_sign_innow fills the user name (from the vault's host list, no secret) and presses Continue BEFORE it asks for the password. A refused reveal issign_in_vault_policy, with ab's words (abSaid),pagePrepared {page, userNameFilled}andwaysOut: 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-nullerroras a failure, soswx_sign_inreported the vault unavailable with an empty message. An error is now a non-empty string, orsuccess: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.exeshowed the 3DEXPERIENCE ID login in a window titled just "3DEXPERIENCE". The bridge saw nothing:swx_start {app:"visualize"}said "launching" andswx_statussaidwaitingOn: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, andswx_status/swx_start(both apps) reportwaitingOn:"userSignIn"withsignInWindow {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_infinds each control withdesktop_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-bridgeskill's everyday flow now covers rendering (swx_render_image, the measured recipe andzoomFactor), 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 anotes[]sentence; no machine-wide workaround is applied.zoomFactormeasured (>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.visualizewas measured reading the real WPF first-run dialog; it now reports the top-levelhwndwhenever 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 azoomFactor(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_imagedraws 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 throughSaveBMP, converts it to PNG, and reads the size back from the file (capped:trueif 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_inis 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_settakestext(0.9.13 sentvalue— every fill would have failed), the UI tree's nodes carryrole/invokable/settableand 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, andrelay_forbiddenreported 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_inrecognizes 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 athelperSignInis 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_progressshows 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 sayswaitingOn:"userSignIn"(the launch gate exits 4), andtools/release.sh --publishis 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 containsLOAD_ADDINand 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; andswx_user_presenceis 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_activewithout 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_activewith the evidence; unreadable counts as present; positive evidence marks the session taken over for good, and onlyuserPresentOverride+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:trueand 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-seenflag 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 staysstartingand the timeout says why; the log's shape (rewritten or appended) is recorded; swx_stop's startup carve-out is pinned touserAuthorized; 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_docsrefused 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_executefailure 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, andswx_statusexposessolidworks.sessionHistory(everHadDocumenttrue/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
StartupProcessCompletedTRUE before the log marker, so both signals stay required): the 3DEXPERIENCE "Connected to …" notice — a title-less#32770tool 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-answerednotificationonly 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.neverAnsweris 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: COMStartupProcessCompletedand 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_startreportsstage:"ready"only then,swx_statusexposessolidworks.readyand what it rests on, and fifteen document and quit verbs refusehost_app_startingwithretryAfterSecondsuntil it is true. A sweep during which SOLIDWORKS dies now sayshost_app_gone— preserved and closed counts, and the session's crash markers from the performance log — instead ofdialog_census_unknown; SOLIDWORKS'~$lock files are listed apart.swx_stoprung (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 underuserAuthorized, on an empty session with no dialog, PRIMES a declined one (one bridge-owned scratch part, ledgeredquit_prime) and asks again. The quit gate's "no longer running" claim had passed vacuously after a decline; it now requiresrunning:falseand is self-tested from the suite. The gates retry ab's fast emptyshell_executefailure 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 toledger-archive-<stamp>.jsonlwith aledger_archivedline 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.jsonlheld only the cache era and the 0.9.0 era sat in a side fileswx_action_ledgernever 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 byat), 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 inmigrated-from-legacy.json(merged), in the ledger (legacy_files_merged) and inswx_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()droppedcreate_dir_all's error and returned the path anyway, so a sweep on a blocked recovery location made one doomedSaveAs3per document. It now refuses once, before the first save, withrecovery_dir_unwritableand 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 ofAdom 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 reportedchangedSinceCopy— inmigrated-from-legacy.json, in the ledger (legacy_file_changed_since_copy), and inswx_recover_list'smigrationblock. 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_listreads all three locations (legacyRecoveryRoots,location:"legacyDataRoot",atRisk), keepsnotCopiedYetatverified: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 fourreferences/*.mdfiles 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.jsonis 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_liststill reads the old location, marks each sweep thereatRiskFromUpdate, 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 isnotCopiedYetwith itslegacyPath— never "gone" — and a half-copied temporary whose original has since disappeared is apartialCopy: 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_listcould not say "your file was closed at its own path, untouched" (measured live 2026-09-30 on Drew'sR_0603_1608Metric_1.step-1.sldprt). The sweep now records such a document asclosedInPlacewith the original's size and mtime, and each listed sweep carriesclosedInPlaceentries 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 thefalseit 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_URLforces the old loopback route); the quit gate passesuserAuthorized: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, andskills/**/SKILL.mdwas 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,.rsand.pyin the repo is scanned and NOTHING is stripped, with a short allowlist of files that DESCRIBE — the README and the named reports underdocs/— 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'sverifiedhard-coded to true,host_failure_entrydroppinghostError, andacknowledge_onlyloosened 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
atexitcannot 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+confirmon 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_importtold 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.verifiedno longer survives a manifest entry that cannot be TIED to the bytes on disk: a missing or nullrecoveryByteswas read as agreement, so a verdict about one file was handed to another — andverified:truemeans the document was closed, so that file is the only copy. The manifest's VERSION is checked, not just the shape offiles. 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 withcreate_dir— the creation IS the claim, where check-then-create raced.GATE PARTIALexits 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, incom.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
finallyAND anatexit, 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.pyrefused 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 runsswx_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.stoppedByDialognow carriestext,buttons,contentsReadableandacknowledgeOnly— read-only,nullwhen unreadable, andneverAnswerstays 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 newran live and the registry drain PASSED —swx_reset_workspaceclosed 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 forkeepUnsaved:trueand 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
Nothingor a realGetImportFileDataobject, 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. Soautomakes ONE LoadFile4 attempt — a future edition that accepts it will simply work — and then fails withinterconnect_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 newproves the same ownership and same-title collision properties withswx_new_document, since the STEP fixture cannot be created on that machine at all — and it states plainly thatswx_close_import_tempsis 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,LoadFile4executes and returns nothing (errors = 1),LoadFile2returns false (still unexplained). PowerShell cannot try alternatives — SOLIDWORKS Connected does not expose the type info its COM adapter needs, so every member call failsTYPE_E_ELEMENTNOTFOUND— which makes this bridge the only instrument. Soswx_importtakesroute,openOptions,docType,withImportDataandargString, echoes the exactinvocationon 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,
LoadFile4finally 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. Soswx_importnow reads that preference, reports it, and routes:OpenDoc6(path, swDocPART, …)when it is on,LoadFile4when it is off. The reply saysinterconnect,linkedandsourcePath, 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:trueis 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 asstoppedByDialog, 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).rgvargis packed reversed, so that argument is the THIRD parameter ofLoadFile4(path, "r", importData, byref errors)—importData— which this bridge had passed asVT_EMPTYsince 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'sNothingis aVT_DISPATCHvariant with a NULLpdispVal, which is now whatLoadFile4gets — and whatswx_com_eval'snullliteral builds, since its documented use isSelectByID2's callout, an[in]object parameter of exactly the same kind. There is still exactly ONELoadFile4call: 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:
LoadFile4had probably never worked live, and the imports of 09-05 to 09-11 went through theLoadFile2fallback — 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_safelytook four documents to a recovery folder and closed every one of them, count 4 to 0, nobody asked to close anything; thenswx_stopquit SOLIDWORKS on rung (a).ExitAppis 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:SaveAs3to a neutral.sldrecovername answers "format not supported", so an unclassifiable document is now named.SLDPRTup front withnamingFallback:"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 amanifest.jsonbeside 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 kindGetTypereported, and whether the extension was a guess.swx_recover_listreports 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, andverified:truefor 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
LoadFile4raising 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 reportshostError.loadFile4(message, detail, code, HRESULT) plus whatLoadFile2actually answered, so "refused" and "threw" are distinguishable.swx_com_evalstays 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_listcalled 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 reportsverifiedwith 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 markedverified:falseare 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
GetTypefails, 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.sldrecovername 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 (SaveAs3dispatches 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.SLDPRTand the record saysunknownKindNamedAsPart— the verification does NOT soften, because the kind is still unknown and the name is only a label. Andswx_recover_listnow gives every file its ownhowToOpen: a.sldrecoverfile 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:
GetTypefailing on a document being closed under memory pressure — the condition the sweep exists for — became "part" viaunwrap_or(1); the recovery file was then NAMED.SLDPRTfrom 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.sldrecovername 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:Assem1is 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.headroomBytesBeforeandestimatedWriteBytesare reported always. A refusal no longer leaves an empty recovery folder behind forswx_recover_listto 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 carry00 00 00 04at 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 andbatchis 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 andexists()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 saysstoppedBy:"dialogCensusUnknown"rather than implying a dialog check that never happened. TheSaveAs3re-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_safelyis the sweep without the quit,swx_recover_listfinds what was preserved, andswx_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-pidnow 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. Andtests/wiring.rsproved "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_attributionshipped 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 intoswx_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 fromswx_stop(at the front gate AND in the per-rung re-sweep),swx_close_all_documentsandswx_reset_workspace, each with a test that drives the VERB and assertsidentity_mismatchin its response — mutation-checked by deleting the CALL SITE, leaving the function and its unit tests untouched. Andtests/wiring.rsnow 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 sessionattributed: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), refusingattached_pid_not_oursotherwise; 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 refuseidentity_mismatchif 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 Noneon that path now names its clause, reported asreattributionRefusedBecauseand 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 noppid, 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 defecthost_app_healthwas 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'sstatusconsults the simulator the way a realGetActiveObjectconsults the machine, so a session that comes up while nobody is callingswx_start— a user finishing a sign-in, which is the entire case this feature exists for — is visible toswx_statusalone. 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.jsonretires 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:explorerandcmdStartcannot 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.
explorerandcmdStarthave 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 aspossiblyOurs— 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 latchedforeignChainSeenon every ordinary launch. That is theforeignChainSeen:trueseen 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 endingsldworksUpNotAttachedrecorded 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 andswx_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 untitledPart1.SLDPRTbeside the user's savedpart1.SLDPRTand 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 — andCloseDocresolves case-insensitively on Windows, so the bridge's untitledPart1.SLDPRTbeside a user's savedpart1.SLDPRTlooked 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, anddonecannot be true). Re-attribution after a sign-in now requires POSITIVE LINEAGE — a parent walk fromsldworks.exethat 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 —
shellExecutehanded back a real handle for the Platform shortcut (CATSTART pid 131272) and that chain reachedRun 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:trueto 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.sldprtthe bridge cannot stat is one SOLIDWORKS overwrites. Nowsymlink_metadatagives 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 thereadyhandshake and a server/host skew is an explicit boot error, becausecloseOutcomeUnknownread withunwrap_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 nowclose_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 aVeclength, so that arm had no fixture at all; every count now goes through one seam thatmock-doccount-nonecan make unreadable.tools/drain-gate.pyalso 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.
donenever consultedWalkStop::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).donenow requires reaching the end ANDdocumentsScanned == documentCountBefore;walkStopis 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 — thatCloseDocaccepts 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 iswrong_document_closed.tools/drain-gate.pyproves 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 titledFirst.step-1.SLDPRTsit 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 intitleCollisionswith every path that shares it; every close is verified withGetOpenDocumentByName, and a close that reports success while the document is still open stops the drain and fails loudly aswrong_document_closed. The mock modelsCloseDoc's by-name resolution, so a close-by-title now fails the suite. A walk that ended early (aGetNexton an invalidated iteration) is no longer reported as complete:walkComplete/walkStoppedare measured anddonerequires 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_docsstill timed out at 30 s and the com host was killed, whileGetDocumentCountanswered in 0.7 s. Above the cap the code calledGetDocuments— one call that marshals every handle — and thenGetPathNameon all 2144 of them to fill a field namedimportTempsInSample. 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 reportsdocumentsScanned;documents()refuses above the cap rather than letting a caller find the wedge;close_all_documentsstill 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_docsin 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 reportedclosed:0, done:truewith "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:tempRootis in the response, documents carrying the launcher's…\swx<pid>\IC~~\<file>tail under an unresolved directory are counted asimportTempsElsewherewith a sample path, and a matched-nothing drain reportsdone:falsewith a hint saying what to point it at. The candidate roots are this process's%TEMP%and the interactive user'sHKCU\Environment\TEMPexpanded — 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~1fordrew, which is what a domain-joined machine really produces besidedrew.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 realExitAppthat 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 aswCefSubProc.exechild; 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_stopv1 calledExitApp, asked the COM running-object table whether SOLIDWORKS was still there, and reported a DECLINE assuccess:true. v2 is a measured ladder — rung (a)ExitAppthrough rung (f) the Restart Manager withoutRmForceShutdown, 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;forcewaives 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.pyis written; no rung has ever run against a real SOLIDWORKS, sodocs/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_tempproved the wrong thing — two tokens anywhere in the path, in any order — so a user's own saved file under folders they had namedTemp\swx-archive\…\IC~~\qualified and would have been closed with no save prompt. It is now a segment walk requiring the launcher's contiguousTemp\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 saysdocument_list_unavailablerather than "no such document". Making the mock report the realistic temp path (asGetPathNamedoes) 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:hungnow carriesverdict:"busy"|"dead"|"unknown"withcpuActiveSeconds/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_statereadGetDocumentCountfirst and, above a cap, return{count, truncated, sample}rather than marshalling thousands of handles for 30s and getting the com host killed. Newswx_close_import_tempsdrains 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_importasserts 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_ownerresolves 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 (notConfirmedwas unreachable after route 1),RouteTrycarries 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 Beginand 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_thanconsults both clocks and distrusts a stamp the filesystem contradicts,timed_outis 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;hungneeds 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_startduring a launch starts nothing); crash-reporter recovery, clicked once and by button name; honesthost_app_hung/host_app_exhausted/host_app_com_unreachablereporting 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_blockednow means only "no shortcut found";swx_run_macroscaffolding; 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.
# 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`](examples/lego-2x4.json), and CSWA exam parts with their published answers in [`resources/cswa/`](resources/cswa/). Measured live on SOLIDWORKS 2026 (30 of 30 checks) and 2021 (a community self-test): [`docs/LIVE-RESULTS.md`](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](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**](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`](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](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
```bash
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:
```bash
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`](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 <version> … 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`](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.