app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
0.9.7 source: adversary pass 44 (no marker history, carve-out pinned, internal args stripped, one dialog classifier, log shape); manifest as published
19 files changed
+644−130
BRIDGE_VERSION+1−1Cargo.lock+1−1Cargo.toml+1−1README.md+14−1bridge.json+1−1docs/ECOSYSTEM-TABLE.md+2−2docs/LAUNCH-RESILIENCE.md+20docs/QUIT-INVESTIGATION.md+18package.json+1−1references/launch-and-sign-in.md+3−1src/perflog.rs+113−82src/quit.rs+42−15src/readiness.rs+41−13src/verbs.rs+12src/verbs3.rs+18tests/adversary42.rs+184tests/e2e.rs+64−1tests/quit.rs+27−2tests/readiness.rs+81−8BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.6+0.9.7
Cargo.lock+1−1@@ -107,7 +107,7 @@ dependencies = [ [[package]] name = "swx-bridge"-version = "0.9.6"+version = "0.9.7" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.9.6"+version = "0.9.7" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+14−1@@ -458,7 +458,20 @@ not. ## Roadmap -- v0.9.6 (this): **priming fires again.** 0.9.5 was measured LIVE on 2026-10-01+- v0.9.7 (this): **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
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "solidworks", "displayName": "SOLIDWORKS",- "version": "0.9.6",+ "version": "0.9.7", "description": "Bridge for SOLIDWORKS desktop CAD \u2014 create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.", "author": "Adom Inc.", "license": "MIT",
docs/ECOSYSTEM-TABLE.md+2−2@@ -112,8 +112,8 @@ templates**, alisamsam **22**. | 44 | **Graceful QUIT that is measured, not assumed** (never a kill, never answers a dialog, proves the exit from the process census) | ✗ | **✓ LIVE-PROVEN 2026-09-17 (rung a)** — `swx_stop {userAuthorized, maxRung:"a"}` quit a bridge-launched session on the FIRST rung (UserControl=false; ExitApp; every COM reference released), verified absent from the process census; `swx_shutdown` then removed a stray empty instance (pid 145188) and `tasklist` showed no `sldworks.exe` at all. **`ExitApp` is NOT declined at zero documents on this edition** — the long-held `host_app_stop_declined` belief was a SAVE-PROMPT STALL misread as a refusal, which is why the ladder was built pinned at rung (a) and why the preserve-then-close sweep is what actually unblocked it. A rung ladder (a `ExitApp` → f Restart Manager *without* `RmForceShutdown`), pinned at rung (a); 7 re-measured preconditions where UNKNOWN refuses and `force` waives the UNSAVED question ONLY; every COM reference released before and after each rung, with outstanding refs named in the report; success = *this pid AND this creation time* absent from the census, never `GetActiveObject`; per-session children reaped while the documented platform daemons are left alone; a dialog stops the ladder permanently and is never answered; nothing disowned until the exit is verified. every precondition re-measured before EVERY rung (a mid-ladder refusal is a real outcome), the exit POLLED to the rung's budget rather than sampled once (a real `ExitApp` returns before the process goes), the dialog census covering the `swCefSubProc` web-view children by parentage, the count cross-checked against the enumeration, and the message rungs re-verifying window ownership before they send. `tools/quit-gate.py` proves the session was bridge-launched, and **its FULL CYCLE PASSED LIVE 2026-09-30 through the bridge ab hosts**: rung (a), process gone 29.5 s, per-session servers reaped, nothing on screen, no crash reporter, every rung reported, and SOLIDWORKS relaunched by the bridge 54.7 s later. The gate now reaches the bridge through ab's verb routing (the side-loaded dev copy on 24695 is terminated by ab 2.1.163 within 30 s) and passes `userAuthorized:true` — the standing authorization — rather than the write-gate pair, which the hosted bridge rightly never enables. **MEASURED 2026-10-01: the decline is about session HISTORY** — rung (a) was declined three times on a session that had never held a document and quit in 3.4 s once one had been created and closed; since 0.9.3 the ladder PRIMES such a session (empty, no dialog, user-authorized only) and asks again — **LIVE-PROVEN 2026-10-01 on 0.9.4**: declined at 130 s, primed, quit in 3.4 s (declines measured from 50 s to 8 min old; such sessions quit unprimed at ~25 s), and `swx_status` exposes the session's document history — and the gate's "no longer running" claim — which had passed vacuously after declines — now requires `running:false` and is self-tested | ✗ | ✗ | ✗ | ✗ | ✗ | | 48 | **A title collision between SAVED documents is impossible here — measured, not assumed** | ✗ | **✓ MEASURED LIVE 2026-09-17** — `tools/drain-gate.py --fixture new` copied a bridge-created document's own file to a second directory under the same basename and called `swx_open` on it. SOLIDWORKS REFUSED: `errors = 65536` (`swFileWithSameTitleAlreadyOpen`). So the drain's title-collision guard — which refuses to close any document whose title another open document shares, and names both paths — can only ever fire for PATHLESS documents, which is exactly the state identity-at-birth removes. The guard stays (a document the bridge did not create can still be pathless, and an older bridge's leftovers are), and the measurement is recorded next to it in `com.rs` so nobody "simplifies" it on the assumption that saved collisions are the common case. No MCP server measures this at all | ✗ | ✗ | ✗ | ✗ | ✗ |-| 51 | **Dialogs judged by MEASURED MODALITY, not by existence** (a notification never blocks; a modal always does) | ✗ | **✓ LIVE-PROVEN 2026-10-01 on 0.9.4** — with the 3DEXPERIENCE "Connected" notice on screen, `swx_stop` quit in 5.1 s and reported it as a notification with its evidence. **0.9.4, from a blocker MEASURED 2026-10-01**: the 3DEXPERIENCE "Connected to …" notice — a title-less `#32770` tool window whose owner frame stayed ENABLED for 95 s, with no Button-class control — blocked every quit, sweep and import for minutes. Now a window blocks when its kind is known-blocking, its owner frame is disabled, or it has a Button-class control; it is a `notification` (reported with owner, ownerEnabled, styles, rect and child controls; never answered) only when all of that was measured; unread evidence fails closed. Template placeholder captions are flagged, never presented as the window's words. **0.9.5 (adversary pass 43): the window's WORDS count — a save/recovery/import kind in its text blocks whatever the evidence, two or more button-like captions in any control class block, and measured notification evidence overrides a licence-type title alone.** No MCP server looks at a dialog at all | ✗ | ✗ | ✗ | ✗ | ✗ |-| 50 | **Readiness gating — attached is not ready** (no document command reaches a SOLIDWORKS that has not finished starting) | ✗ | **✓ 0.9.3, from a crash MEASURED 2026-10-01**: `swx_new_document` 8 s after COM attach, `swx_open` at 9 s and `swx_close_all_safely` at 12 s crashed SOLIDWORKS while its 3DEXPERIENCE add-in was still initialising (performance log: LOAD_ADDIN still running, SWEXH/UNHANDLED, the 3DEXPERIENCE marker only after the document commands, SWERR_REPORT); a control launch with no document commands crashed nothing. Now a READY stage follows `attached`, decided by two independent signals — COM `StartupProcessCompleted` and the performance log's 3DEXPERIENCE marker after the last LOAD_ADDIN — with an honest grace and timeout that never promote to ready; every verb that reaches SOLIDWORKS refuses `host_app_starting` with `retryAfterSeconds` until then — **0.9.5: gated by default with a pinned 16-verb exemption list, the performance log bound to this session's process creation time (an earlier session's log is never "complete"), COM alone never enough on a machine that writes markers, an on-demand add-in told from a start by quiet + which add-in, the grace unlocking reads only, and the window-message quit rungs ungated under userAuthorized; LIVE 2026-10-01 a document verb was refused during startup. 0.9.6: every gated reply carries the readiness its gate evaluated, the log is read from its per-start header, and the on-demand rule keys on the platform DLL SWK3DCompassAddin.dll**; a sweep during which SOLIDWORKS dies says `host_app_gone` with the log's crash markers. The rescue held under the crash (26,129 bytes verified). No MCP server distinguishes attached from ready at all | ✗ | ✗ | ✗ | ✗ | ✗ |+| 51 | **Dialogs judged by MEASURED MODALITY, not by existence** (a notification never blocks; a modal always does) | ✗ | **✓ LIVE-PROVEN 2026-10-01 on 0.9.4** — with the 3DEXPERIENCE "Connected" notice on screen, `swx_stop` quit in 5.1 s and reported it as a notification with its evidence. **0.9.4, from a blocker MEASURED 2026-10-01**: the 3DEXPERIENCE "Connected to …" notice — a title-less `#32770` tool window whose owner frame stayed ENABLED for 95 s, with no Button-class control — blocked every quit, sweep and import for minutes. Now a window blocks when its kind is known-blocking, its owner frame is disabled, or it has a Button-class control; it is a `notification` (reported with owner, ownerEnabled, styles, rect and child controls; never answered) only when all of that was measured; unread evidence fails closed. Template placeholder captions are flagged, never presented as the window's words. **0.9.5 (adversary pass 43): the window's WORDS count — a save/recovery/import kind in its text blocks whatever the evidence, two or more button-like captions in any control class block, and measured notification evidence overrides a licence-type title alone; 0.9.7: swx_dismiss_blocking_dialogs uses the same title-and-words classifier before it closes anything.** No MCP server looks at a dialog at all | ✗ | ✗ | ✗ | ✗ | ✗ |+| 50 | **Readiness gating — attached is not ready** (no document command reaches a SOLIDWORKS that has not finished starting) | ✗ | **✓ 0.9.3, from a crash MEASURED 2026-10-01**: `swx_new_document` 8 s after COM attach, `swx_open` at 9 s and `swx_close_all_safely` at 12 s crashed SOLIDWORKS while its 3DEXPERIENCE add-in was still initialising (performance log: LOAD_ADDIN still running, SWEXH/UNHANDLED, the 3DEXPERIENCE marker only after the document commands, SWERR_REPORT); a control launch with no document commands crashed nothing. Now a READY stage follows `attached`, decided by two independent signals — COM `StartupProcessCompleted` and the performance log's 3DEXPERIENCE marker after the last LOAD_ADDIN — with an honest grace and timeout that never promote to ready; every verb that reaches SOLIDWORKS refuses `host_app_starting` with `retryAfterSeconds` until then — **0.9.5: gated by default with a pinned 16-verb exemption list, the performance log bound to this session's process creation time (an earlier session's log is never "complete"), COM alone never enough on a machine that writes markers, an on-demand add-in told from a start by quiet + which add-in, the grace unlocking reads only, and the window-message quit rungs ungated under userAuthorized; LIVE 2026-10-01 a document verb was refused during startup. 0.9.6: every gated reply carries the readiness its gate evaluated, the log is read from its per-start header, and the on-demand rule keys on the platform DLL SWK3DCompassAddin.dll; 0.9.7 (LIVE-PROVEN 0.9.6): no marker history — a disabled platform add-in never locks the bridge out, and a log still being written holds even a machine's first start**; a sweep during which SOLIDWORKS dies says `host_app_gone` with the log's crash markers. The rescue held under the crash (26,129 bytes verified). No MCP server distinguishes attached from ready at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 49 | **The user's rescued work survives the bridge's own updates — and ab's** (recovery never lives in a directory anything else owns) | ✗ | **✓ 0.9.1: the data root is `%LOCALAPPDATA%\adom-solidworks-bridge\`, a directory only this bridge writes to** — moved out of `Adom Bridge\` altogether after adversary pass 39 found 0.9.0's `Adom Bridge\solidworks-data` sitting inside the directory ab's installer and uninstaller own, whose fate on an ab uninstall or reinstall is documented nowhere; both earlier locations are migrated oldest first, every copy recorded by (root, path, size, mtime, sha256) so a legacy file that changes after its copy is copied again BESIDE it and reported `changedSinceCopy`, never settled by name — and since 0.9.2 a small file whose size and mtime match is hashed too, and two versions of the ledger or the ownership registry are MERGED (union, both versions kept beside) rather than one picked; a temp-directory fallback makes every sweep refuse (`data_root_is_temp`), and an unwritable recovery folder is refused once before the first save (`recovery_dir_unwritable`). **0.9.0, from a SEV-1 MEASURED 2026-09-30**: `bridges-cache\solidworks` was found CREATED 2026-09-18 09:42 by ab's auto-install of 0.8.9 — the updater re-extracts the whole directory, and every recovery folder from the day before, the ledger and the ownership registry went with it (test documents only; nothing of Drew's was lost). 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 (copied under a temporary name and renamed into place once verified byte for byte — so a start that dies mid-copy never leaves a truncated rescue under the real name — never over an existing file, originals left in place, each file once, a copy that does not verify removed and retried next start); `swx_recover_list` still reads the old location and marks each rescue there `atRiskFromUpdate`, saying whether a verified copy exists — and in the data root calls a file whose copy has not landed `notCopiedYet` (pointing at its only copy), never "gone"; and a sweep REFUSES (`recovery_inside_install_dir`, nothing written, nothing closed) if its recovery folder would land under the install directory or the install cache — however the path is spelled (links, `..`, case). Every rule mutation-verified. No MCP server keeps a recovery folder at all | ✗ | ✗ | ✗ | ✗ | ✗ | | 47 | **Interconnect-aware import: refuses honestly, with the reason and the setting** | ✗ | **✓ MEASURED + shipped 0.8.2** — on SOLIDWORKS Connected 2026 with `swMultiCAD_Enable3DInterconnect` TRUE there is NO programmatic neutral-file import route at all: LoadFile4 returns nothing (`errors=1`) both with `Nothing` and with a real `GetImportFileData` object, LoadFile2 returns false, OpenDoc6 answers `swFileRequiresRepairError` (2097152) across four option values and `swInvalidFileTypeError` (1024) for docType 0. So `swx_import` makes ONE LoadFile4 attempt (a future edition that accepts it simply works) 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**. No MCP server reads that toggle at all: they report a translator failure, which sends the reader hunting a missing translator that is not the problem | ✗ | ✗ | ✗ | ✗ | ✗ | | 46 | **3D Interconnect is understood, obeyed and reported** (the user's own setting decides how a neutral file comes in; the bridge reads it and never writes it) | ✗ | **✓ LIVE-MEASURED 2026-09-17, routed in 0.8.0** — `GetUserPreferenceToggle(691)` = TRUE on the install, so a STEP is not translated at all: `LoadFile4` executes and returns nothing with `errors=1`, and SOLIDWORKS instead OPENS the file with `OpenDoc6(path, swDocPART, …)` as a part holding a LINKED 3D Interconnect feature. `swx_import` reads the toggle, routes accordingly, and reports `interconnect` / `linked` / `sourcePath` so a caller knows the part is not self-contained. The preference is NEVER written — not even flip-and-restore, because a crash between the halves would leave the user's CAD configured differently than they left it — and `dissolve:true` is REFUSED until the link-breaking call is measured on this edition. Import-diagnostics auto-run (toggle 291) is also TRUE, so the modal dialog it raises is classified, reported as `stoppedByDialog`, and never answered. Toggle ids read from the installed `SolidWorks.Interop.swconst.dll` | ✗ | ✗ | ✗ | ✗ | ✗ |
docs/LAUNCH-RESILIENCE.md+20@@ -1119,6 +1119,26 @@ ownership sweep PASS; drain gate 5/5. The performance log: gate evaluated it (`gatePassedFor:"windowMessageRungs"` on a quit let through during startup), so a live run can prove no COM verb ran while starting. +**Measured live on 0.9.6 (2026-10-01), and 0.9.7 (adversary pass 44).** Launch+gate PASSED (16/16, READY at 32.3 s); a `swx_new_document` reply carried+`readiness {ready:true, state:"ready", evaluatedAt:"gate"}`; sweep PASS; drain+5/5; quit gate 11/11. On THIS start the marker `<3DXINITERROR>0` was the LAST+line of the log (105 of 105) — so a clean start sometimes writes a `LOAD_ADDIN`+after its marker (the 0.9.5 measurement, line 106 → 107) and sometimes does not.+Both are recorded; the rule handles both (complete at once, or on demand after+the quiet window).++| 0.9.7 | |+|---|---|+| The marker-history flag is GONE | `perflog-markers-seen` made two kinds of start wrong. F2: once set, a session that never loads the platform add-in (a user may disable `SWK3DCompassAddin.dll` — 24 s of the start) read `loading` forever. F3: before it was set, a log with no marker let COM decide alone — the first launch on any machine was the 10-01 crash window. The rule no longer depends on history |+| A log this session is still writing holds the start | No marker, the log written less than 60 s ago: `loading` — COM's `true` does not promote it (measured: COM true before the marker) |+| The platform add-in loaded, its marker never came | `loading`, however quiet the log (a dialog, a sign-in); past five minutes `readinessTimedOut`, and the refusal says WHY (the log's own reason) |+| The platform add-in never loaded | Log quiet for 60 s and no `LOAD_ADDIN` naming `SWK3DCompassAddin.dll`: no marker is coming — `platformAddInNotLoaded`, ready only with COM's StartupProcessCompleted |+| The log's shape, recorded (F7) | `performanceLog.shape` = `{lengthNow, headersInTail, model}` (`rewrittenPerStart` / `appended` / `oneHeaderInTail` / `noHeader`), plus `lengthAtFirstSight` and `shrankSinceFirstSight` (a rewrite shrinks it). An appended log is read from its last start header, like a rewritten one |++**Residual:** an edition that writes no start header AND appends would still be+read as one block; the mtime binding is then the only tie to the session.+ ## Live proof 4 — 0.9.3 on Drew's machine (2026-10-01) | | Measured |
docs/QUIT-INVESTIGATION.md+18@@ -2060,3 +2060,21 @@ measured, its owner's enablement was not. blocking dialog (the only case that tells the reader to hand a screenshot to the user), and refuses on an unreadable census without blaming the user's screen. Its `--self-test` pins all four verdicts.++### 5.21 LIVE 2026-10-01 on 0.9.6 — priming fixed, twice; 0.9.7 closes adversary pass 44++* **Priming, live:** an empty relaunched session at ≥ 120 s old → `rungsTried`+ [declined 30.7 s, quit `primed:true` 3.4 s] — twice. The QUIT GATE PASSED+ (11/11, with relaunch) and printed the bridge's dialog census instead of+ refusing on the Connected notification.+* **MH pinned (F4):** swx_stop's startup carve-out needs `userAuthorized`;+ without it a quit during startup is `host_app_starting` with no rung tried —+ never the window-message rungs.+* **Internal arguments (F5):** every underscore-prefixed argument is stripped+ from caller input at the dispatcher, so `_startupNotVerified` cannot be used on+ a ready session to skip the gentle COM rungs.+* **One classifier (F6):** `quit::classify_words` reads a window's title AND its+ words; `judge_window` and `swx_dismiss_blocking_dialogs` both use it. Dismiss+ reads a window's words before closing it: a save question under a "Warning"+ title is refused as `save_changes` "by its text", and a window whose words+ cannot be read is not closed.
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "solidworks-bridge",- "version": "0.9.6",+ "version": "0.9.7", "type": "app", "description": "Skills for your container so your AI knows how to drive the SOLIDWORKS bridge (swx_* verbs). The bridge runtime itself is the release zip — a single native swx-bridge.exe; Adom Bridge loads that.", "tags": [
references/launch-and-sign-in.md+3−1@@ -84,7 +84,9 @@ Read this before starting SOLIDWORKS, while waiting through a launch or a 3DEXPE before the start finishes (measured), so it is never enough alone on a machine whose log writes 3DEXPERIENCE markers. An add-in loaded on demand after the start (a quiet log, an add-in other than 3DEXPERIENCE, COM true)- does not make a started session "starting" again.+ does not make a started session "starting" again. A session that never+ loads the platform add-in (it can be disabled) is ready once its log is quiet+ and COM agrees; a log still being written holds the start. - **`state:"unverifiable"`** — nothing can verify the start. After the grace (`readsAllowed:true`) READ verbs run; anything that creates, opens, saves, closes or changes a document refuses with `startingUnverifiable:true`
src/perflog.rs+113−82@@ -74,24 +74,30 @@ pub const ON_DEMAND_QUIET_SECS: u64 = 60; /// What the log says about the RUNNING session's start. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LogVerdict {- /// No log, or a log that has never held a 3DEXPERIENCE marker (an edition- /// that does not write one): the log cannot judge, and is not used.+ /// No log at all, or one that cannot be read: the log cannot judge, and COM+ /// decides. Unavailable,- /// The edition writes markers, but nothing ties the log to THIS session: no- /// process creation time, or a log last written before the process started- /// (adversary pass 42: a day-old log ending in a completed start read- /// "complete" for a session that had not written a line). Not ready.+ /// Nothing ties the log to THIS session: no process creation time, or a log+ /// last written before the process started (adversary pass 42: a day-old log+ /// ending in a completed start read "complete" for a session that had not+ /// written a line). COM alone never decides then. Not ready. NotThisSession,- /// The latest `LOAD_ADDIN` is newer than the latest marker and the log is- /// still being written: a start is loading. Not ready.+ /// This session's log is still being written with add-ins loading and no+ /// marker after them (or the platform add-in loaded and its marker has not+ /// come): a start is loading. Not ready. Loading, /// A marker after the latest `LOAD_ADDIN`, in a log written since the /// process started. Ready, by the log. Complete,- /// A marker, then a `LOAD_ADDIN`, then nothing for `ON_DEMAND_QUIET_SECS`:- /// an add-in loaded on demand after the start completed (adversary pass 42).- /// Ready, by the log — ONLY together with COM's StartupProcessCompleted.+ /// A marker, then a `LOAD_ADDIN` that is not the platform add-in, then+ /// nothing for `ON_DEMAND_QUIET_SECS`: an add-in loaded on demand after the+ /// start completed. Ready — ONLY together with COM's StartupProcessCompleted. OnDemandAfterComplete,+ /// This session never loaded the platform add-in (`SWK3DCompassAddin.dll`+ /// — the one that writes the marker; a user may disable it), its log has+ /// gone quiet: there is no marker to wait for (adversary pass 44, F2). Ready+ /// — ONLY together with COM's StartupProcessCompleted.+ PlatformAddInNotLoaded, } impl LogVerdict {@@ -102,18 +108,39 @@ impl LogVerdict { LogVerdict::Loading => "loading", LogVerdict::Complete => "complete", LogVerdict::OnDemandAfterComplete => "onDemandAddInAfterComplete",+ LogVerdict::PlatformAddInNotLoaded => "platformAddInNotLoaded", } } } +fn mtime_secs(path: &std::path::Path) -> Option<u64> {+ std::fs::metadata(path)+ .and_then(|md| md.modified())+ .ok()+ .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())+ .map(|d| d.as_secs())+}+ /// HAS THIS SESSION'S START FINISHED, by the log? `session_start` is the-/// process creation time in epoch seconds — the bridge knows it from the process-/// census — and the log is only allowed to speak for the session if it was last-/// written at or after it. The log's lines carry no timestamps this bridge can-/// rely on, so within the log the latest start is located by position: its-/// `LOAD_ADDIN` lines, then its 3DEXPERIENCE marker. Measured 2026-10-01: COM's-/// StartupProcessCompleted was already TRUE before the marker of a real start, so-/// a log that cannot be tied to the session never lets COM decide alone.+/// process creation time in epoch seconds, and the log may speak for the+/// session only if it was last written at or after it. MEASURED 2026-10-01:+/// the file is REWRITTEN at every start and begins with a header; only the text+/// after the last header is read. Within it:+///+/// * a marker after the last `LOAD_ADDIN` -> complete;+/// * a `LOAD_ADDIN` after the marker: the platform DLL -> loading;+/// anything else, log quiet >= ON_DEMAND_QUIET_SECS -> on demand;+/// log still being written -> loading;+/// * NO marker: log still being written (quiet < the window) -> loading —+/// a log THIS session is actively writing is evidence on its own, whatever+/// history says (F3: the first start on a machine was the crash window);+/// quiet, the platform DLL loaded (its marker never came) -> loading;+/// quiet, the platform DLL never loaded (disabled) -> no marker+/// to wait for: `platformAddInNotLoaded` (F2).+///+/// MEASURED: COM's StartupProcessCompleted was TRUE before the marker of a real+/// start, so a log that cannot be tied to the session never lets COM decide+/// alone, and a log still being written holds COM's `true`. pub fn startup_complete(session_start: Option<u64>) -> (LogVerdict, String) { let Some(path) = path() else { return (LogVerdict::Unavailable, "no SOLIDWORKS performance log to read".into());@@ -121,46 +148,19 @@ pub fn startup_complete(session_start: Option<u64>) -> (LogVerdict, String) { let Some(t) = tail() else { return (LogVerdict::Unavailable, format!("the performance log {} could not be read", path.display())); };- // THE FILE IS REWRITTEN AT EVERY START, beginning with a header (measured- // 2026-10-01 on 0.9.5: 1271 lines after the crash session, then 107 and 124- // on the next starts; first line `; SolidWorks generated log file - do not- // edit.`). Only what follows the last header belongs to the running start.- if LOAD_MARKERS.iter().any(|m| t.contains(m)) {- remember_markers_seen();- } let t: &str = match t.rfind(LOG_HEADER) { Some(h) => &t[h..], None => &t, };- let last_marker = LOAD_MARKERS.iter().filter_map(|m| t.rfind(m).map(|i| (i, *m))).max();- // DOES THIS MACHINE'S SOLIDWORKS WRITE MARKERS AT ALL? Once a marker has been- // seen in this log it is remembered (data root), so a tail that has rolled- // past every old marker — or a session that has not written its own yet —- // never turns the log back into "no signal" and lets COM decide alone.- if last_marker.is_some() {- remember_markers_seen();- }- if last_marker.is_none() && !markers_seen_before() {- return (- LogVerdict::Unavailable,- "the performance log has never held a 3DEXPERIENCE load marker on this machine, so it \- cannot judge"- .into(),- );- }- let mtime = std::fs::metadata(&path)- .and_then(|md| md.modified())- .ok()- .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())- .map(|d| d.as_secs());+ let mtime = mtime_secs(&path); let Some(start) = session_start else { return ( LogVerdict::NotThisSession, "this SOLIDWORKS process's creation time is unknown, so the log cannot be tied to it".into(), ); };- match mtime {- Some(mt) if mt >= start => {}+ let mt = match mtime {+ Some(mt) if mt >= start => mt, Some(mt) => { return ( LogVerdict::NotThisSession,@@ -172,39 +172,63 @@ pub fn startup_complete(session_start: Option<u64>) -> (LogVerdict, String) { ) } None => return (LogVerdict::NotThisSession, "the log's modification time could not be read".into()),- }+ };+ let quiet = crate::launchos::epoch_secs().saturating_sub(mt);+ let is_platform = |l: &str| l.contains("LOAD_ADDIN") && l.to_ascii_lowercase().contains(PLATFORM_ADDIN_DLL);+ let last_marker = LOAD_MARKERS.iter().filter_map(|m| t.rfind(m).map(|i| (i, *m))).max(); let Some((m, name)) = last_marker else {+ let addins = t.lines().filter(|l| l.contains("LOAD_ADDIN")).count();+ let platform = t.lines().any(is_platform);+ if quiet < ON_DEMAND_QUIET_SECS {+ return (+ LogVerdict::Loading,+ format!(+ "this session's log is still being written ({quiet}s ago, {addins} LOAD_ADDIN \+ line(s)) and holds no 3DEXPERIENCE marker yet: a start in progress — held until \+ the marker arrives or the log has been quiet for {ON_DEMAND_QUIET_SECS}s"+ ),+ );+ }+ if platform {+ return (+ LogVerdict::Loading,+ format!(+ "the platform add-in ({PLATFORM_ADDIN_DLL}) loaded in this session but its marker \+ has not been written, and the log has been quiet for {quiet}s: the platform \+ load has not finished (a dialog, a sign-in?)"+ ),+ );+ } return (- LogVerdict::Loading,- "this machine's SOLIDWORKS writes a 3DEXPERIENCE load marker, and none is in the log \- since this session began writing to it"- .into(),+ LogVerdict::PlatformAddInNotLoaded,+ format!(+ "this session never loaded the platform add-in ({PLATFORM_ADDIN_DLL}), so no marker \+ is coming; {addins} LOAD_ADDIN line(s) and no log activity for {quiet}s (>= \+ {ON_DEMAND_QUIET_SECS}s): the add-in load phase is over — ready only if COM also \+ says the start completed"+ ), ); }; let last_addin = t.rfind("LOAD_ADDIN"); match last_addin { Some(a) if a > m => {- let quiet = mtime.map(|mt| crate::launchos::epoch_secs().saturating_sub(mt)).unwrap_or(0);- // A START loads the 3DEXPERIENCE PLATFORM add-in — it is what writes- // the marker — so a LOAD_ADDIN for it after the last marker is a start- // in progress, however quiet the log has gone (wedged behind a- // dialog). Keyed on the DLL NAME (measured 2026-10-01): three add-ins- // carry "3DEXPERIENCE" in their names — SaveFolderTo3DEXPERIENCEAddin2+ // A START loads the PLATFORM add-in — it is what writes the marker —+ // so a LOAD_ADDIN for it after the last marker is a start in+ // progress, however quiet the log (wedged behind a dialog). Keyed on+ // the DLL NAME (measured 2026-10-01): three add-ins carry+ // "3DEXPERIENCE" in their names — SaveFolderTo3DEXPERIENCEAddin2 // ("Batch Save to 3DEXPERIENCE"), TaskPaneCacheExplorerAddin // ("3DEXPERIENCE files on This PC") and SWK3DCompassAddin // ("3DEXPERIENCE for WORKS"); only the last is the platform.- let after = &t[m..];- let start_block = after.lines().any(|l| {- l.contains("LOAD_ADDIN") && l.to_ascii_lowercase().contains(PLATFORM_ADDIN_DLL)- });+ let start_block = t[m..].lines().any(is_platform); if quiet >= ON_DEMAND_QUIET_SECS && !start_block { ( LogVerdict::OnDemandAfterComplete, format!(- "a {name} marker, then LOAD_ADDIN line(s) not naming the platform add-in (SWK3DCompassAddin.dll), \- then no log activity for {quiet}s (>= {ON_DEMAND_QUIET_SECS}s): an add-in \- loaded on demand after the start completed — ready only if COM also says the \- start completed"+ "a {name} marker, then LOAD_ADDIN line(s) not naming the platform add-in \+ ({PLATFORM_ADDIN_DLL}), then no log activity for {quiet}s (>= \+ {ON_DEMAND_QUIET_SECS}s): an add-in loaded on demand after the start \+ completed — ready only if COM also says the start completed" ), ) } else {@@ -213,7 +237,11 @@ pub fn startup_complete(session_start: Option<u64>) -> (LogVerdict, String) { format!( "add-ins are still loading: the last LOAD_ADDIN is newer than the last {name}{}, \ and the log was written {quiet}s ago",- if start_block { " and names the platform add-in SWK3DCompassAddin.dll (a start, not an on-demand load)" } else { "" }+ if start_block {+ " and names the platform add-in (a start, not an on-demand load)"+ } else {+ ""+ } ), ) }@@ -225,20 +253,23 @@ pub fn startup_complete(session_start: Option<u64>) -> (LogVerdict, String) { } } -fn markers_flag() -> PathBuf {- crate::scratch::root().join("perflog-markers-seen")-}--fn markers_seen_before() -> bool {- markers_flag().exists()-}--fn remember_markers_seen() {- let f = markers_flag();- if !f.exists() {- let _ = std::fs::create_dir_all(crate::scratch::root());- let _ = std::fs::write(&f, crate::launchos::epoch_secs().to_string());- }+/// THE FILE'S SHAPE, for the record (adversary pass 44, F7): its length now and+/// how many start headers its tail holds. MEASURED 2026-10-01: rewritten at+/// every start (one header, at the top). An APPENDED log would hold several —+/// the reading above still takes only the text after the last one.+pub fn shape() -> serde_json::Value {+ let Some(p) = path() else { return serde_json::Value::Null };+ let len = std::fs::metadata(&p).map(|m| m.len()).ok();+ let t = tail().unwrap_or_default();+ let headers = t.matches(LOG_HEADER).count();+ let whole = len.map(|l| l <= TAIL_BYTES).unwrap_or(false);+ let model = match headers {+ 0 => "noHeader",+ 1 if whole && t.starts_with(LOG_HEADER) => "rewrittenPerStart",+ 1 => "oneHeaderInTail",+ _ => "appended",+ };+ serde_json::json!({"lengthNow": len, "headersInTail": headers, "model": model}) } /// WHAT SOLIDWORKS WROTE ON ITS WAY DOWN: the last lines of the tail that carry
src/quit.rs+42−15@@ -270,23 +270,13 @@ pub fn judge_window( contents: Option<&crate::launchos::DialogContents>, ) -> (bool, &'static str, String) { let title_kind = crate::abwin::classify(&w.title).0;- // THE WINDOW'S OWN WORDS (adversary pass 43, W1): the classifier runs over- // the body text and button captions as well as the title. SOLIDWORKS titles- // its prompts "SOLIDWORKS" and puts the question in the body.- let body_kind = contents.and_then(|c| {- c.text- .iter()- .chain(c.buttons.iter())- .map(|t| crate::abwin::classify(t).0)- .find(|k| *k != "unknown")- });+ // ONE CLASSIFIER for the title AND the words (adversary passes 43 and 44):+ // the same function swx_dismiss_blocking_dialogs uses before it closes.+ let (words_kind, source, _, _) = classify_words(&w.title, contents); // A save, recovery or import prompt blocks whatever the evidence says — by // title OR by text. No measured modality overrides these.- if NEVER_OVERRIDDEN_KINDS.contains(&title_kind) {- return (true, title_kind, format!("a known blocking kind ({title_kind}) by its title"));- }- if let Some(k) = body_kind.filter(|k| NEVER_OVERRIDDEN_KINDS.contains(k)) {- return (true, k, format!("a known blocking kind ({k}) by its text"));+ if NEVER_OVERRIDDEN_KINDS.contains(&words_kind) {+ return (true, words_kind, format!("a known blocking kind ({words_kind}) by its {source}")); } // Any other title kind blocks by title — EXCEPT a licence-type title, which // measured notification evidence may override (W2): the 3DEXPERIENCE@@ -340,6 +330,43 @@ pub fn judge_window( } } +/// THE ONE CLASSIFIER over a window's TITLE and its WORDS (text and button+/// captions): `(kind, source, blocking, neverAutoAnswer)`. A save, recovery or+/// import kind from EITHER wins; otherwise the title's kind; otherwise the first+/// kind the words name; otherwise `unknown` (never auto-answered). `source` is+/// "title", "text" or "none". Used by `judge_window` and by+/// swx_dismiss_blocking_dialogs, so a save question in a window titled+/// "Warning" is a save question to both.+pub fn classify_words(+ title: &str,+ contents: Option<&crate::launchos::DialogContents>,+) -> (&'static str, &'static str, bool, bool) {+ let t = crate::abwin::classify(title);+ let words: Vec<(&'static str, bool, bool)> = contents+ .map(|c| {+ c.text+ .iter()+ .chain(c.buttons.iter())+ .map(|x| crate::abwin::classify(x))+ .filter(|k| k.0 != "unknown")+ .collect()+ })+ .unwrap_or_default();+ if NEVER_OVERRIDDEN_KINDS.contains(&t.0) {+ return (t.0, "title", t.1, t.2);+ }+ if let Some(k) = words.iter().find(|k| NEVER_OVERRIDDEN_KINDS.contains(&k.0)) {+ return (k.0, "text", k.1, k.2);+ }+ if t.0 != "unknown" {+ return (t.0, "title", t.1, t.2);+ }+ if let Some(k) = words.first() {+ return (k.0, "text", k.1, k.2);+ }+ (t.0, "none", t.1, t.2)+}+ /// Kinds that block whatever the measured evidence says — a choice about the /// user's work (save), their recovered work (recovery), or the geometry an /// import produces.
src/readiness.rs+41−13@@ -62,6 +62,9 @@ fn grace_secs() -> u64 { struct Seen { first: Instant, ready: bool,+ /// The performance log's length when this session was first seen (F7): a+ /// rewrite at the next start shrinks it; an appended log only grows.+ log_len_at_first_sight: Option<u64>, } static SESSIONS: Mutex<Option<HashMap<i64, Seen>>> = Mutex::new(None);@@ -101,7 +104,12 @@ pub fn assess(ctx: &crate::verbs::Ctx) -> Value { let mut guard = SESSIONS.lock().unwrap_or_else(|e| e.into_inner()); let map = guard.get_or_insert_with(HashMap::new); let key = pid.unwrap_or(-1);- let seen = map.entry(key).or_insert(Seen { first: Instant::now(), ready: false });+ let shape = crate::perflog::shape();+ let len_now = shape.get("lengthNow").and_then(|v| v.as_u64());+ let seen = map+ .entry(key)+ .or_insert(Seen { first: Instant::now(), ready: false, log_len_at_first_sight: len_now });+ let len_first = seen.log_len_at_first_sight; let age = seen.first.elapsed().as_secs(); let mut based_on: Vec<&str> = Vec::new(); // Some(true) verified ready, Some(false) verified starting, None unverifiable.@@ -121,10 +129,12 @@ pub fn assess(ctx: &crate::verbs::Ctx) -> Value { (L::Complete, _) => Some(true), (L::OnDemandAfterComplete, Some(true)) => Some(true), (L::OnDemandAfterComplete, None) => None,+ (L::PlatformAddInNotLoaded, Some(true)) => Some(true),+ (L::PlatformAddInNotLoaded, None) => None, (L::Unavailable, Some(true)) => Some(true), (L::Unavailable, None) => None,- // The edition writes markers, none is this session's: COM alone is- // never enough (it answers true before the first marker).+ // Nothing ties the log to this session: COM alone is never enough+ // (it answers true before the first marker). (L::NotThisSession, _) => None, } };@@ -160,12 +170,23 @@ pub fn assess(ctx: &crate::verbs::Ctx) -> Value { "processCreatedAt": created, "basedOn": based_on, "startupProcessCompleted": com,- "performanceLog": {"verdict": log.name(), "why": log_why},- "rule": "ready = the performance log shows THIS session's start complete (COM not \- false), or COM true with an on-demand add-in after the session's marker, or COM \- true on an edition that writes no markers. COM alone is never enough while the \- edition writes markers and none is this session's. Unverifiable sessions allow \- read verbs after the grace; never document commands.",+ "performanceLog": {+ "verdict": log.name(),+ "why": log_why,+ "shape": shape,+ "lengthAtFirstSight": len_first,+ "shrankSinceFirstSight": match (len_first, len_now) {+ (Some(a), Some(b)) => json!(b < a),+ _ => Value::Null,+ },+ },+ "rule": "ready = this session's performance log (the text after its last start header, \+ written since the process started) shows the start complete with COM not false; \+ or COM true and the log quiet 60 s after an on-demand add-in that is not the \+ platform DLL, or quiet 60 s with the platform DLL never loaded (no marker is \+ coming); or COM true with no log at all. A log still being written holds COM's \+ true; a log not tied to this session never lets COM decide alone. Unverifiable \+ sessions allow read verbs after the grace; never document commands.", "secondsSinceFirstSeen": age, "readinessTimedOut": !ready && age >= TIMEOUT_SECS, })@@ -254,14 +275,21 @@ fn refusal_for(r: &Value, verb: &str, touch: Touch) -> Option<Value> { (the grace); this one does not. Look at swx_status's `solidworks.readiness` \ (`performanceLog.why` says why the log cannot vouch for this session) and at the \ user's screen. Nothing was sent to SOLIDWORKS."+ .to_string() } else if timed_out {- "SOLIDWORKS has not reported a finished start for over five minutes. It is still \- treated as starting — the clock never promotes it to ready. Look at swx_status's \- `solidworks.readiness` and at the user's screen (a dialog, a sign-in); do not \- force document commands through."+ format!(+ "SOLIDWORKS has not reported a finished start for over five minutes. It is still \+ treated as starting — the clock never promotes it to ready. Why: {}. Look at \+ swx_status's `solidworks.readiness` and at the user's screen (a dialog, a \+ sign-in); do not force document commands through.",+ r.pointer("/performanceLog/why").and_then(|v| v.as_str())+ .or_else(|| r.get("startupProcessCompleted").and_then(|v| v.as_bool()).filter(|c| !c).map(|_| "COM's StartupProcessCompleted is false"))+ .unwrap_or("no signal says the start finished")+ ) } else { "Wait `retryAfterSeconds`, then call again — or poll swx_status until \ `solidworks.ready` is true. Nothing was sent to SOLIDWORKS."+ .to_string() }, ); if let Value::Object(m) = &mut out {
src/verbs.rs+12@@ -3142,6 +3142,18 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value { // sent 8 s after COM attach crashed SOLIDWORKS while its 3DEXPERIENCE add-in // was still initialising. Reads may run on an unverifiable session after the // grace; mutations never do. See src/readiness.rs.+ // INTERNAL ARGUMENTS ARE NOT CALLER INPUT (adversary pass 44, F5). Every+ // underscore-prefixed key is the dispatcher's own (`_startupNotVerified`+ // skips the gentle COM quit rungs); a caller who passes one has it removed+ // here, before the gate and the handler see it.+ let stripped: Value;+ let args: &Value = match args {+ Value::Object(m) if m.keys().any(|k| k.starts_with('_')) => {+ stripped = Value::Object(m.iter().filter(|(k, _)| !k.starts_with('_')).map(|(k, v)| (k.clone(), v.clone())).collect());+ &stripped+ }+ _ => args,+ }; let mut gated_args: Option<Value> = None; // WHAT THE GATE SAW goes into every gated verb's reply, success or refusal // (0.9.6, G3), so a live run can prove no COM verb ran while starting.
src/verbs3.rs+18@@ -3294,6 +3294,24 @@ pub fn dismiss_blocking_dialogs(ctx: &Ctx, args: &Value) -> Value { })); continue; }+ // READ ITS WORDS, with the classifier judge_window uses (adversary pass+ // 44, F6): a save question in a window titled "Warning" is a save+ // question. A window whose words cannot be read is not closed.+ let title = d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();+ let words = match crate::launchos::os().dialog_contents(h) {+ Ok(c) => c,+ Err(e) => {+ refused.push(json!({"hwnd": h, "title": title,+ "why": format!("its words could not be read ({e}), so whether it asks about the user's work is unknown")}));+ continue;+ }+ };+ let (wkind, wsource, _, wnever) = crate::quit::classify_words(&title, Some(&words));+ if wnever {+ refused.push(json!({"hwnd": h, "title": title, "kind": wkind,+ "why": format!("by its {wsource}, this dialog is a '{wkind}' question about the user's own work")}));+ continue;+ } let r = crate::abwin::close_window(h, &reason, AB_TIMEOUT); let ok_flag = r.get("success").and_then(|v| v.as_bool()).unwrap_or(false); crate::ledger::record("dialog_closed", json!({"hwnd": h, "title": d.get("title"), "ok": ok_flag, "reason": reason}));
tests/adversary42.rsadded+184@@ -0,0 +1,184 @@+//! ADVERSARIAL REVIEW OF 0.9.5 — pass forty-four.+//!+//! readiness.rs, perflog.rs, quit.rs (judge_window) and the dispatch gate+//! compile here and were exercised. com.rs (the real StartupProcessCompleted)+//! and LiveOs are cfg(windows): READ, NOT RUN.+//!+//! The log lines below use the format the coordinator measured on Drew's+//! machine, verbatim:+//! <AD2> "LOAD_ADDIN" "SWK3DCompassAddin.dll" "3DEXPERIENCE for WORKS" [2.18] "6.34.0.2027" 14 COM.new 1770885081 13+//! <AD2> "LOAD_ADDIN" "TaskPaneCacheExplorerAddin.DLL" "3DEXPERIENCE files on This PC" …+//!+//! WHAT CHECKS OUT+//! * (R4) The exempt verbs: window_info / dismiss_blocking_dialogs /+//! close_window / screenshot / set_driving_badge make no com-host call;+//! swx_kill reads `app_state` (a count, not a document command), as its+//! exemption says. dismiss_blocking_dialogs refuses anything with+//! neverAutoAnswer (defaulting TRUE when absent), then needs the+//! process-level write opt-in, then ownerConfirmed. close_window needs+//! confirm + reason. One residual: dismiss classifies by TITLE only+//! (`abwin::classify`), where judge_window now reads the body — a window+//! titled "…Warning…" / "…Rebuild…" (kind `message`, answerable) whose body+//! is a save question is closable by dismiss under the opt-in. A window+//! close is normally Cancel, not Discard; sev-3, the two classifiers should+//! be one.+//! * (R5) swx_stop's startup carve-out only skips COM rungs a/b/e; the+//! preconditions (P2 zero unsaved, no blocking dialog) run at verbs.rs:1189+//! before the ladder for every quit. Note `_startupNotVerified` is an+//! ordinary argument a caller can pass on a READY session; it only skips the+//! gentle COM rungs. Strip it from caller args.+//! * (R6) Captions count only as whole-control matches and it takes TWO, so+//! the word "OK" inside a sentence, or a lone "Close", does not block; a+//! notice with separate "OK" and "Close" controls would (safe direction).+//! Non-English captions match nothing and fall through to the modality+//! evidence — a localised custom-drawn save prompt would again read as a+//! notification. Drew's install is English.+//! * (R1) The code ASSUMES the latest marker belongs to the latest start+//! (position, no timestamps) and binds only by file mtime >= process start.+//! Under the REWRITTEN-per-session behaviour you measured (1271 -> 107 lines)+//! that holds. Under an APPENDED log it does not: a new session's first+//! non-LOAD_ADDIN line refreshes the mtime and the previous session's marker+//! reads `complete`. Nothing in the code detects which model the file+//! follows; recording the file length when a session is first seen (a+//! rewrite shrinks it) would. Not tested — not reachable on the measured+//! behaviour.++mod common;+use common::*;+use serde_json::{json, Value};++fn s(v: &Value) -> String {+ serde_json::to_string_pretty(v).unwrap_or_default()+}++fn session(scratch: &TempDir) {+ std::fs::write(+ scratch.join("mock-solidworks-open-docs.json"),+ json!({"running": true, "untitled": 1, "active": Value::Null, "docs": [], "files": {}}).to_string(),+ )+ .unwrap();+}++fn write_log(scratch: &TempDir, text: &str, age_secs: u64) -> String {+ let log = scratch.join("SolidWorksPerformance.log");+ std::fs::write(&log, text).unwrap();+ std::fs::File::options()+ .write(true)+ .open(&log)+ .unwrap()+ .set_modified(std::time::SystemTime::now() - std::time::Duration::from_secs(age_secs))+ .unwrap();+ log.to_string_lossy().to_string()+}++const COMPASS: &str = r#"<AD2> "LOAD_ADDIN" "SWK3DCompassAddin.dll" "3DEXPERIENCE for WORKS" [2.18] "6.34.0.2027" 14 COM.new 1770885081 13"#;+const TASKPANE: &str = r#"<AD2> "LOAD_ADDIN" "TaskPaneCacheExplorerAddin.DLL" "3DEXPERIENCE files on This PC" [1.0] "6.34.0.2027" 14 COM.new 1770885090 13"#;+const TOOLBOX: &str = r#"<AD2> "LOAD_ADDIN" "SWToolbox.dll" "SOLIDWORKS Toolbox Library" [1.0] "34.0" 14 COM.new 1770885070 13"#;+const MARKER: &str = "<3DXLOAD_TIME>24000</3DXLOAD_TIME>";++/// SEVERITY 2 — "NAMES 3DEXPERIENCE" MATCHES THE WRONG ADD-IN.+///+/// The on-demand rule refuses to call a post-marker LOAD_ADDIN "on demand" if+/// the line contains "3DEXPERIENCE", reasoning that the 3DEXPERIENCE add-in is+/// what writes the marker, so loading it again means a start. Two add-ins on+/// Drew's machine contain that word. The one that writes the marker is+/// `SWK3DCompassAddin.dll` ("3DEXPERIENCE for WORKS"). The other,+/// `TaskPaneCacheExplorerAddin.DLL` ("3DEXPERIENCE files on This PC"), is a task-+/// pane browser — exactly the kind of add-in that loads on demand. When it does,+/// after the marker, the session reads `loading` however quiet the log is, COM's+/// "completed" is out-voted, and every COM verb is refused for the rest of the+/// session: the pass-42 never-ready bug, back for this one add-in. (And if this+/// add-in happens to load AFTER the marker during an ordinary start — the order+/// was not measured — no session on that machine would ever be ready.)+///+/// Key the rule on the DLL that writes the marker (`SWK3DCompassAddin.dll`), not+/// on a word in the description.+#[test]+fn the_3dexperience_file_browser_loaded_on_demand_is_not_a_start() {+ let scratch = TempDir::new("adv42-taskpane");+ session(&scratch);+ let text = format!("{TOOLBOX}\n{COMPASS}\n{MARKER}\n{TASKPANE}\n");+ let logp = write_log(&scratch, &text, 300);+ let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_new_document", json!({"type": "part"}));+ assert!(+ r["readiness"]["startupProcessCompleted"] == json!(true) || r["success"] == json!(true),+ "precondition: COM says the start completed: {}", s(&r)+ );+ assert!(+ r["errorCode"] != json!("host_app_starting"),+ "COM says the start completed, the marker was written by SWK3DCompassAddin.dll, and five \+ quiet minutes later the only post-marker LOAD_ADDIN is TaskPaneCacheExplorerAddin.DLL \+ (\"3DEXPERIENCE files on This PC\") — a file browser loaded on demand. Because its \+ description contains the word 3DEXPERIENCE, the session is \"loading\" for good and every \+ COM verb is refused. Key on the DLL that writes the marker. {}",+ s(&r)+ );+}++/// SEVERITY 2 — A USER WHO DISABLES THE 3DEXPERIENCE ADD-IN CAN NEVER USE THE+/// BRIDGE AGAIN.+///+/// Once `perflog-markers-seen` exists, a session whose log holds no marker reads+/// `loading` ("none is in the log since this session began") — forever, since+/// the add-in that writes it never loads. Disabling that add-in is a common thing+/// to do (it is the slow one: 24 s of the measured start). Every COM verb is then+/// refused for every session, `readinessTimedOut` arrives at five minutes, and+/// nothing promotes it. The flag is permanent in the data root.+///+/// A start that did not load SWK3DCompassAddin.dll has no marker to wait for. If+/// the log shows this session's add-ins loaded, no 3DEXPERIENCE start in it, and+/// has gone quiet — and COM says complete — that is a finished start.+#[test]+fn a_session_without_the_3dexperience_add_in_is_not_starting_forever() {+ let scratch = TempDir::new("adv42-disabled");+ session(&scratch);+ std::fs::write(scratch.join("perflog-markers-seen"), "1").unwrap();+ let logp = write_log(&scratch, &format!("{TOOLBOX}\n"), 300);+ let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_new_document", json!({"type": "part"}));+ assert!(+ r["errorCode"] != json!("host_app_starting"),+ "the 3DEXPERIENCE add-in is disabled, so this session never loads it and never writes a \+ marker. COM says the start completed, the log shows only Toolbox loading and has been \+ quiet for five minutes — and the session is \"loading\" forever because this machine has \+ EVER seen a marker. Every COM verb refused for every session from now on. {}",+ s(&r)+ );+}++/// SEVERITY 2 — THE FIRST START AFTER INSTALLING 0.9.5 IS THE CRASH WINDOW.+///+/// With no `perflog-markers-seen` flag yet, a log with no marker is `unavailable`+/// and COM decides alone — and COM was MEASURED true before the marker on a real+/// start. The flag is new in 0.9.5, and under the rewritten-per-session log the+/// previous session's markers are gone by the time a new start is assessed. So+/// on every machine, the first launch under 0.9.5 that is asked for readiness+/// while loading has COM alone deciding: ready, during add-in load — the+/// 2026-10-01 crash. (The rescue held that day; the crash is not data loss, which+/// is why this is sev-2.)+///+/// A log that THIS session is actively writing with LOAD_ADDIN lines is evidence+/// of loading on its own, marker history or not. Treat it as `loading` until it+/// goes quiet or a marker arrives, with a timeout that says so.+#[test]+fn a_log_being_written_with_add_ins_loading_holds_the_first_start() {+ let scratch = TempDir::new("adv42-firststart");+ session(&scratch);+ assert!(!scratch.join("perflog-markers-seen").exists(), "precondition: no marker ever seen here");+ let logp = write_log(&scratch, &format!("{TOOLBOX}\n{COMPASS}\n"), 0);+ let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_new_document", json!({"type": "part"}));+ assert!(+ r["errorCode"] == json!("host_app_starting"),+ "the log was written this second, its last lines are LOAD_ADDIN — the 3DEXPERIENCE add-in \+ among them — and no marker yet: a start in progress. Because this machine has never had a \+ marker RECORDED (the flag is new in 0.9.5), the log is ignored and COM — measured true \+ BEFORE the marker — decides alone: swx_new_document ran. The first launch under 0.9.5 on \+ every machine is the 2026-10-01 crash window. {}",+ s(&r)+ );+}
tests/e2e.rs+64−1@@ -2212,7 +2212,20 @@ fn the_ab_delegated_half_is_gated_and_dispatches_nothing_when_refused() { // With the opt-in, the bridge's OWN export sheet is closable — the narrow // case this verb exists for.- let on = Bridge::start_with(&[("ADOM_DIRECT_API_URL", &ab.url), ("SWX_ALLOW_USER_WRITES", "1")]);+ // The export sheet's WORDS are read before it is closed (0.9.7, F6: one+ // classifier with judge_window), so the scenario supplies them.+ let words = TempDir::new("dismiss-words");+ std::fs::write(words.join("windows.json"), "[]").unwrap();+ std::fs::write(+ words.join("dialog-open.json"),+ json!({"hwnd": 858186, "text": ["Export options"], "buttons": ["OK", "Cancel"]}).to_string(),+ )+ .unwrap();+ let on = Bridge::start_with(&[+ ("ADOM_DIRECT_API_URL", &ab.url),+ ("SWX_ALLOW_USER_WRITES", "1"),+ ("SWX_LAUNCH_FAKE_DIR", &words.path()),+ ]); on.call("swx_start", json!({})); let on_state = on.call("swx_get_app_state", json!({})); let on_pid = on_state["processId"].as_i64().unwrap_or(sw_pid);@@ -2241,6 +2254,56 @@ fn the_ab_delegated_half_is_gated_and_dispatches_nothing_when_refused() { ); } +/// ONE CLASSIFIER (0.9.7, adversary pass 44 F6): swx_dismiss_blocking_dialogs+/// reads a window's WORDS with the classifier judge_window uses before it+/// closes anything. A window titled "Warning" (kind `message`, answerable by+/// title) whose body is a save question is NOT closed under the opt-in; a+/// window whose words cannot be read is not closed either.+#[test]+fn a_save_question_under_a_warning_title_is_never_dismissed() {+ let ab = common::AbStub::start(json!([]));+ let words = TempDir::new("dismiss-warning-words");+ std::fs::write(words.join("windows.json"), "[]").unwrap();+ std::fs::write(+ words.join("dialog-open.json"),+ json!({"hwnd": 858187, "text": ["Do you want to save changes to Part1.SLDPRT?"],+ "buttons": ["Save", "Don't Save", "Cancel"]}).to_string(),+ )+ .unwrap();+ let on = Bridge::start_with(&[+ ("ADOM_DIRECT_API_URL", &ab.url),+ ("SWX_ALLOW_USER_WRITES", "1"),+ ("SWX_LAUNCH_FAKE_DIR", &words.path()),+ ]);+ on.call("swx_start", json!({}));+ let st = on.call("swx_get_app_state", json!({}));+ let pid = st["processId"].as_i64().unwrap();+ ab.set_windows(json!([+ {"hwnd": st["mainHwnd"].as_i64().unwrap(), "title": "SOLIDWORKS", "className": "Afx:0001", "pid": pid, "rect": {}},+ {"hwnd": 858187, "title": "Warning", "className": "#32770", "pid": pid, "rect": {}},+ {"hwnd": 858188, "title": "Rebuild Errors", "className": "#32770", "pid": pid, "rect": {}}+ ]));+ let r = on.call("swx_dismiss_blocking_dialogs", json!({+ "close": true, "confirm": true, "allowUserWrites": true, "reason": "tidying"+ }));+ check!(on, "nothing was closed", r["closed"].as_array().map(|c| c.is_empty()).unwrap_or(false), s(&r));+ let refused = r["refused"].as_array().cloned().unwrap_or_default();+ check!(+ on,+ "the 'Warning' window is refused BY ITS TEXT as a save question",+ refused.iter().any(|x| x["hwnd"] == 858187 && x["kind"] == "save_changes"+ && x["why"].as_str().unwrap_or("").contains("by its text")),+ s(&r)+ );+ check!(+ on,+ "a window whose words cannot be read is refused, not closed",+ refused.iter().any(|x| x["hwnd"] == 858188 && x["why"].as_str().unwrap_or("").contains("could not be read")),+ s(&r)+ );+ check!(on, "NOTHING destructive reached ab", ab.destructive_calls().is_empty(), format!("{:?}", ab.destructive_calls()));+}+ #[test] fn a_host_record_from_another_solidworks_session_is_not_evidence() { // M1: the persisted record carried no session id and was cleared only on a
tests/quit.rs+27−2@@ -903,6 +903,13 @@ fn the_window_message_rungs_run_during_startup_with_the_com_rungs_skipped() { refused["errorCode"] == "host_app_starting" && refused["windowMessageRungs"].is_string(), s(&refused) );+ let noauth = b.call("swx_stop", full_args("d"));+ check!(+ b,+ "...never without the user's authorization: host_app_starting, no rung tried",+ noauth["errorCode"] == "host_app_starting" && noauth["rungsTried"].is_null(),+ s(&noauth)+ ); let r = b.call("swx_stop", authorized("d")); let tries = r["rungsTried"].as_array().cloned().unwrap_or_default(); check!(@@ -916,6 +923,24 @@ fn the_window_message_rungs_run_during_startup_with_the_com_rungs_skipped() { && tries[2]["result"] == "quit", s(&r) );- let noauth = b.call("swx_stop", full_args("d"));- check!(b, "...never without the user's authorization", noauth["stopped"] != true, s(&noauth));+}++/// INTERNAL ARGUMENTS ARE STRIPPED FROM CALLER INPUT (0.9.7, adversary pass 44+/// F5). `_startupNotVerified` makes the ladder skip its gentle COM rungs; a+/// caller who passes it on a READY session must not get that — rung (a) runs.+#[test]+fn a_caller_cannot_skip_the_gentle_rungs_with_an_internal_argument() {+ let m = Sim::new("quit-internal-arg", json!({"a": "exited"}));+ let b = m.boot(&[]);+ b.call("swx_start", json!({}));+ let mut args = authorized("d");+ args["_startupNotVerified"] = json!(true);+ let r = b.call("swx_stop", args);+ let tries = r["rungsTried"].as_array().cloned().unwrap_or_default();+ check!(+ b,+ "swx_stop: rung (a) ran and quit — the internal argument was ignored",+ r["stopped"] == true && tries.first().map(|t| t["rung"] == "a" && t["result"] == "quit").unwrap_or(false),+ s(&r)+ ); }
tests/readiness.rs+81−8@@ -153,11 +153,19 @@ fn the_performance_log_is_a_second_independent_witness() { let r = b.call("swx_new_document", json!({"type": "part"})); check!(b, "swx_new_document: runs once the 3DEXPERIENCE marker follows the add-ins", r["success"] == true, s(&r)); - // A LOG WITH NO 3DEXPERIENCE MARKER AT ALL cannot judge, and is not used.+ // A SESSION THAT NEVER LOADED THE PLATFORM ADD-IN (0.9.7, F2): no marker+ // is coming. Its log has gone quiet after its add-ins, and COM says the+ // start completed: ready, and it says why. let scratch2 = TempDir::new("ready-perflog-none"); session(&scratch2, json!([])); let log2 = scratch2.join("SolidWorksPerformance.log"); std::fs::write(&log2, "<LOAD_ADDIN> SolidWorks Toolbox\n").unwrap();+ std::fs::File::options()+ .write(true)+ .open(&log2)+ .unwrap()+ .set_modified(std::time::SystemTime::now() - std::time::Duration::from_secs(300))+ .unwrap(); let logp2 = log2.to_string_lossy().to_string(); let b2 = Bridge::start_in(TempDir::borrowed(&scratch2.0), &[("SWX_PERF_LOG", &logp2)]); // The FIRST assessment says what it was based on; later ones say the session@@ -165,10 +173,10 @@ fn the_performance_log_is_a_second_independent_witness() { let st = b2.call("swx_start", json!({})); check!( b2,- "swx_start: a log without a 3DEXPERIENCE marker is not a signal — COM alone decides, and says so",+ "swx_start: platform add-in never loaded, log quiet, COM true => ready, basis platformAddInNotLoaded", st["ready"] == true- && st["readiness"]["performanceLog"]["verdict"] == "unavailable"- && st["readiness"]["basedOn"] == json!(["StartupProcessCompleted"]),+ && st["readiness"]["performanceLog"]["verdict"] == "platformAddInNotLoaded"+ && st["readiness"]["basedOn"] == json!(["StartupProcessCompleted", "performanceLog"]), s(&st) ); let st2 = b2.call("swx_status", json!({}));@@ -473,18 +481,17 @@ fn com_alone_never_vouches_for_a_session_with_no_marker_of_its_own() { s(&r) ); - // This session is writing the log, the machine is known to write markers,- // and none of this session's is in it yet: still loading.+ // This session is writing the log right now, with add-ins loading and no+ // marker yet: still loading (0.9.7: no marker history needed). let scratch2 = TempDir::new("ready-no-marker-yet"); session(&scratch2, json!([]));- std::fs::write(scratch2.join("perflog-markers-seen"), "1").unwrap(); let logp2 = write_log(&scratch2, "<LOAD_ADDIN> SolidWorks Toolbox\n<LOAD_ADDIN> 3DEXPERIENCE\n", 0); let b2 = Bridge::start_in(TempDir::borrowed(&scratch2.0), &[("SWX_PERF_LOG", &logp2)]); b2.call("swx_start", json!({})); let r2 = b2.call("swx_new_document", json!({"type": "part"})); check!( b2,- "swx_new_document: a machine that writes markers, none yet in this session => loading, refused",+ "swx_new_document: a log being written with add-ins loading and no marker yet => loading, refused", r2["errorCode"] == "host_app_starting" && r2["readiness"]["performanceLog"]["verdict"] == "loading", s(&r2) );@@ -697,3 +704,69 @@ fn a_blip_or_an_interrupted_count_never_records_a_document() { s(&st["solidworks"]["sessionHistory"]) ); }++/// THE PLATFORM ADD-IN LOADED AND ITS MARKER NEVER CAME: held, however quiet+/// the log (a dialog, a sign-in) — and the log's SHAPE is recorded: a file that+/// begins with the start header is rewritten per start; two headers is an+/// appended log, of which only the last section is read (0.9.7, F2/F7).+#[test]+fn a_platform_load_without_its_marker_is_held_and_the_logs_shape_is_recorded() {+ const COMPASS: &str = r#"<AD2> "LOAD_ADDIN" "SWK3DCompassAddin.dll" "3DEXPERIENCE for WORKS" [2.18] "6.34.0.2027" 14 COM.new 1770885081 13"#;+ let scratch = TempDir::new("ready-platform-no-marker");+ session(&scratch, json!([]));+ let logp = write_log(+ &scratch,+ &format!("; SolidWorks generated log file - do not edit.\n<VERSION> 6</VERSION>\n{COMPASS}\n"),+ 300,+ );+ let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_new_document", json!({"type": "part"}));+ check!(+ b,+ "swx_new_document: the platform add-in loaded, no marker, quiet => loading, refused",+ r["errorCode"] == "host_app_starting"+ && r["readiness"]["performanceLog"]["verdict"] == "loading"+ && r["readiness"]["performanceLog"]["shape"]["model"] == "rewrittenPerStart"+ && r["readiness"]["performanceLog"]["lengthAtFirstSight"].is_u64(),+ s(&r)+ );++ // No platform add-in, a quiet log — but COM cannot answer: that verdict+ // needs COM's "completed" too, so the session is unverifiable.+ let scratch = TempDir::new("ready-no-platform-no-com");+ session(&scratch, json!([]));+ std::fs::write(scratch.join("mock-startup-unreadable"), "").unwrap();+ let logp = write_log(&scratch, "<LOAD_ADDIN> \"SWToolbox.dll\"\n", 300);+ let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_new_document", json!({"type": "part"}));+ check!(+ b,+ "platformAddInNotLoaded without COM's word is not a verified start",+ r["errorCode"] == "host_app_starting"+ && r["startingUnverifiable"] == true+ && r["readiness"]["performanceLog"]["verdict"] == "platformAddInNotLoaded",+ s(&r)+ );++ let scratch = TempDir::new("ready-appended");+ session(&scratch, json!([]));+ let logp = write_log(+ &scratch,+ "; SolidWorks generated log file - do not edit.\n<LOAD_ADDIN> old\n<3DXLOAD_TIME>24000</3DXLOAD_TIME>\n\+ ; SolidWorks generated log file - do not edit.\n<VERSION> 6</VERSION>\n<LOAD_ADDIN> \"SWToolbox.dll\"\n",+ 0,+ );+ let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);+ b.call("swx_start", json!({}));+ let r = b.call("swx_new_document", json!({"type": "part"}));+ check!(+ b,+ "an APPENDED log: the earlier start's marker is not read; this start is loading",+ r["errorCode"] == "host_app_starting"+ && r["readiness"]["performanceLog"]["verdict"] == "loading"+ && r["readiness"]["performanceLog"]["shape"]["model"] == "appended",+ s(&r)+ );+}