← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.10.4+0.10.5
Cargo.lock+1−1
@@ -122,7 +122,7 @@ dependencies = [  [[package]] name = "swx-bridge"-version = "0.10.4"+version = "0.10.5" dependencies = [  "miniz_oxide",  "serde",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.10.4"+version = "0.10.5" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+31−1
@@ -199,6 +199,12 @@ instead of ever attempting an install. The verbs that need no COM `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",@@ -492,7 +498,31 @@ not.  ## Roadmap -- v0.10.4 (this): **proven live on SOLIDWORKS 2026.** 0.10.3 passed 30 of 30+- v0.10.5 (this): **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
SKILL.md+5
@@ -29,6 +29,7 @@ BEFORE you do it: | start SOLIDWORKS, wait through a launch or a 3DEXPERIENCE sign-in, or decide whether it is ready | `references/launch-and-sign-in.md` | | write anything (save, export over a file, set properties, run a script or macro, close someone's document) or touch a dialog or window | `references/writes-and-dialogs.md` | | model, import a STEP/IGES, export, or map a Fusion 360 habit onto SOLIDWORKS | `references/modelling-and-export.md` |+| run anything long (a test battery, many new parts, batch exports or renders), or see a low-memory warning | the **solidworks-memory** skill |  ## The rules that never bend @@ -134,6 +135,10 @@ BEFORE you do it:    is the user's to answer — the bridge reports    `waitingOn:"userVisualizeContentFolder"` and never clicks Continue, because    that creates a folder on their machine. `swx_status.visualize` has the rest.+10. Long runs: read swx_status.memory first; see the solidworks-memory skill.+   While committed memory is critical, verbs that create or load geometry are+   refused with `low_commit_memory`; the reply names who holds the memory and+   the one safe remedy. Never close the user's other applications.  Files land on the DESKTOP filesystem; pull exports back with ab's `pull_file {"filePaths":["C:\\...png"]}` (they arrive under /tmp on this machine).
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "solidworks",   "displayName": "SOLIDWORKS",-  "version": "0.10.4",+  "version": "0.10.5",   "description": "Bridge for SOLIDWORKS desktop CAD — create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.",   "author": "Adom Inc.",   "license": "MIT",
docs/COMPATIBILITY.md+14−6
@@ -315,15 +315,23 @@ from the gate, so a check held at the gate is reported, not hidden.   On 2026 they are now MEASURED ([LIVE-RESULTS.md](LIVE-RESULTS.md)). On 2021   they are not, and each step's `proven.live` says `false` there. +  Eight more variants were measured on 2026 later the same day, with bridge+  0.10.4 ([LIVE-RESULTS.md](LIVE-RESULTS.md)):+  - a reversed boss;+  - a boss through all, one way;+  - Right Plane's axes;+  - sketches on the +X and −Z side faces;+  - a plane offset from Front Plane;+  - a two-direction linear pattern;+  - an outward shell.+   Some variants have not been measured on any release yet. Steps that use them   report `proven.live:false` and name them under `unmeasured`:-  - a boss with its direction reversed;-  - through all on a boss;-  - a sketch on Right Plane, on a named plane, on a plane offset from Front or-    Right, or on a side face;-  - patterns along another axis, or in two directions;+  - through all with `direction:"both"` on a boss;+  - a sketch on a named plane, on a plane offset from Right Plane or from a named+    plane, on a negative offset from Front Plane, or on a face facing −X or +Z;+  - a linear pattern along Y or Z alone, and a circular pattern about X or Z;   - a chamfer on all of a feature's edges;-  - an outward shell;   - a colour on one body of a multi-body part;   - the later rungs of the selection ladder;   - the CreateDefinition fillet fallback.
docs/LIVE-RESULTS.md+25−1
@@ -57,7 +57,31 @@ Built in 4 features, measured live: | T24 | the LEGO 2x4 acceptance brick (examples/lego-2x4.json) | #1 acceptance | 2073.362 | 2073.362 | PASS | | T23 | issue #2: run_script on the bridge's own new part (addressed by path) says bridgeCreatedDocument:true | #2.b |  |  | PASS | +## Added 2026-10-14 (bridge 0.10.4)++Eight more checks were added to `tools/dsl-live-check.py` and run on the same+install (SOLIDWORKS 2026, major 34, 3DEXPERIENCE "Connected") with bridge+**0.10.4**. All eight pass. T36 and T37 first failed on the checks' own+expectations, not on the bridge. A face sketch keeps the default plane's axes+and projects the origin onto the face, so their circles were moved to sketch+(0, 5) and both checks re-run. Since 0.10.5 these variants report+`proven.live: true` with this measurement in `measuredOn`.++| Id | Check | Issue | Expected volume (mm^3) | Measured (mm^3) | Measured bounding box (mm) | Result |+|---|---|---|---|---|---|---|+| T33 | a REVERSED boss: reverse:true from Top Plane goes -Y | #1.2 | 4000.0 | 4000.0 | [-10, -10, -10] to [10, 0, 10] | PASS |+| T34 | a boss through all (one way) from a plane below the block | #1.2 | 4062.832 | 4062.832 | [-10, -5, -10] to [10, 10, 10] | PASS |+| T35 | Right Plane axes: sketch +X is model -Z, +Y is model +Y, a boss goes +X | #1.5 | 1000.0 | 1000.0 | [0, 0, -10] to [5, 20, 0] | PASS |+| T36 | a sketch on the +X side face extrudes outward along +X (Right Plane's axes, origin projected) | #1.1 | 4037.699 | 4037.699 | [-10, 0, -10] to [13, 10, 10] | PASS |+| T37 | a sketch on the -Z face extrudes outward along -Z (Front Plane's axes, origin projected) | #1.1 | 4037.699 | 4037.699 | [-10, 0, -13] to [10, 10, 10] | PASS |+| T38 | a plane offset +12 mm from Front Plane (a separate body at z 12..14) | #1.1 | 4032.0 | 4032.0 | two bodies, the second [-2, -2, 12] to [2, 2, 14] | PASS |+| T39 | a linear pattern in two directions (X +12, Z -12): 4 bosses on the block | #1.6 | 4084.823 | 4084.823 | [-10, 0, -10] to [10, 13, 10] | PASS |+| T42 | a shell outward 1 mm with the top face removed: the block becomes the cavity | #1.6 | 1324.0 | 1324.0 | [-11, -1, -11] to [11, 10, 11] | PASS |+ ## Not yet measured  - SOLIDWORKS **2021** (and any version other than 2026): supported by the API audit in `docs/COMPATIBILITY.md`, not yet run. `swx_selftest {"level":"write"}` on that install answers it.-- `throughAll` with `direction:"both"` on a boss, the `IEntity::Select4` and `SelectByRay` selection rungs (never needed live: the first rung worked), and the `CreateDefinition` fillet fallback (never needed: `FeatureFillet3` worked).+- `throughAll` with `direction:"both"` on a boss, the `IEntity::Select4`, `SelectByRay` and `SelectByID2` selection rungs (never needed live: the first rung worked), and the `CreateDefinition` fillet fallback (never needed: `FeatureFillet3` worked).+- Sketches on a plane offset from Right Plane, a negative offset from Front Plane, or an offset from a named plane; sketches on a named plane; sketches on faces facing -X or +Z.+- A chamfer on all of a feature's edges, a colour on one body of a multi-body part, and `new_assembly`, `save_as`, `rebuild` and `zoom_fit` as script steps.+- `swx_describe` lists every variant and its measurement as `liveMeasured` (one table, `compat::OP_FACTS`).
install.sh+2
@@ -17,4 +17,6 @@ install_skill() { # <name> <source dir>   done } install_skill solidworks-sign-in "$H/skills/solidworks-sign-in"+# A dependent skill: keeping SOLIDWORKS from running the PC out of memory.+install_skill solidworks-memory "$H/skills/solidworks-memory" echo "[solidworks-bridge] skills installed."
package.json+2−1
@@ -1,6 +1,6 @@ {   "slug": "solidworks-bridge",-  "version": "0.10.4",+  "version": "0.10.5",   "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": [@@ -28,6 +28,7 @@     "references/writes-and-dialogs.md",     "references/modelling-and-export.md",     "skills/solidworks-sign-in/SKILL.md",+    "skills/solidworks-memory/SKILL.md",     "examples/lego-2x4.json",     "package.json",     "install.sh",
skills/solidworks-memory/SKILL.mdadded+69
@@ -0,0 +1,69 @@+---+name: solidworks-memory+description: "How to keep SOLIDWORKS from running a Windows PC out of memory while an AI drives it - what the SOLIDWORKS Resource Monitor 'available committed memory is critically low' warning means, why SOLIDWORKS keeps memory after documents close, how to read the bridge's memory report before and during long runs, when and how to recycle a bridge-started session safely, and what never to close. Use before any long or repeated SOLIDWORKS run (test batteries, many new parts, batch exports, renders), whenever swx_status reports low commit memory, and whenever the user mentions the Resource Monitor warning, low memory, commit charge, SOLIDWORKS slowing down or a PC running out of memory. Trigger words - solidworks memory, resource monitor, committed memory critically low, commit charge, out of memory, low memory warning, solidworks slow, memory leak, recycle solidworks, long run, test battery, many parts."+---++Parent skill: **solidworks-bridge**++# Keeping SOLIDWORKS from running the PC out of memory++## What the warning means++- [measured] The **SOLIDWORKS Resource Monitor** toast "Available committed memory is+  critically low. Close some applications immediately to reduce the commit charge" is about+  **committed** memory (the commit charge against the commit limit: RAM plus the page file),+  not free RAM. A PC can show gigabytes of free RAM and still be out of commit.+- [measured] On 2026-10-14 it fired on a 31.4 GB laptop at **72.3 of 76.6 GB committed (94 %)**.+  The biggest holders were Fusion 360 (20.7 GB, running two days), SOLIDWORKS (10.6 GB),+  Chrome (10.2 GB over 51 processes) and WSL (4.7 GB).+- Running out of commit makes allocations fail in every application, not only SOLIDWORKS:+  unsaved work anywhere on the machine is at risk. Treat the warning as urgent.++## Why an AI-driven session grows++- [measured] SOLIDWORKS does **not** give memory back when documents close. A session the+  bridge started for a test run held **10.6 GB committed with zero documents open**, after+  roughly a hundred scratch parts were built and closed in it (about 100 MB per part).+- [measured] Quitting that session through the bridge (`swx_stop`) returned **about 16 GB**+  of commit (72.3 to 56.1 GB): SOLIDWORKS plus its helper processes.+- So the cost is per document created in the session, not per document open. Long runs+  (test batteries, `swx_selftest {"level":"write"}`, batch exports, many `new_part`+  scripts) are what fill the machine.++## The rules++1. **Read memory before a long run, and between its steps.** `swx_status` reports a+   `memory` block (commit used, limit and available; SOLIDWORKS's own committed bytes; the+   largest holders). [unmeasured: the bridge reports it from 0.10.5; not yet read on a+   live machine] If commit+   available is low before you start, do not start: tell the user what holds the memory.+2. **Recycle a session the bridge started, never one the user is working in.** When+   SOLIDWORKS's own commit passes a budget (6 GB is a sensible default on a 32 GB PC) or+   commit available drops under about 10 % of the limit, and the session was started by+   the bridge and has **zero documents open**: `swx_stop {"userAuthorized": true, "reason": ...}`+   then `swx_start` (1-3 minutes on a 3DEXPERIENCE edition). `swx_stop` refuses on its own+   when documents are open or the user is active; never work around that refusal.+3. **Close what you made as you go.** `swx_reset_workspace` after each scratch document, or+   let the script close it. Closing does not free the memory, but it keeps the session+   recyclable.+4. **Never close the user's applications.** Not Fusion, not the browser, not anything else,+   however much it holds. Report the largest holders with their sizes and let the user+   decide. Restarting a long-running Fusion or browser is the user's call.+5. **Never change the page file or SOLIDWORKS settings** to make room. Those are the user's.+6. **Split long runs.** A battery of more than about 30 new parts should recycle the+   bridge's session between batches. [unmeasured] `swx_selftest {"level":"write"}` does+   this itself from 0.10.5: before each check it reads memory, recycles a session the+   bridge started with nothing open (its reply lists `recycled`), and otherwise stops with+   `stoppedFor:"memory"` rather than end a session that is not its own.+7. **A refusal is not an obstacle to route around.** [unmeasured] While commit is+   critical, verbs that create or load geometry answer `low_commit_memory` and change+   nothing; the reply names the holders and the safe remedy. `ignoreMemoryPressure:true`+   with a reason runs the verb anyway and is ledgered: use it only when the user says so.++## Diagnosing by hand (read-only)++The commit numbers Windows shows in Task Manager > Performance > Memory as "Committed".+From the container, a read-only PowerShell over ab can read+`Win32_PerfFormattedData_PerfOS_Memory` (`CommittedBytes`, `CommitLimit`,+`PercentCommittedBytesInUse`) and the top processes by `PrivateMemorySize64`. Report the+numbers; change nothing.
src/compat.rs+115−31
@@ -99,7 +99,12 @@ impl Measurement { /// {level:"write"} 43 passed / 0 failed / 1 skipped (docs/LIVE-RESULTS.md). pub const LIVE_2026_10_14: Measurement = Measurement { year: 2026, major: 34, edition: "connected", date: "2026-10-14", bridge: "0.10.3" }; +/// The same day, bridge 0.10.4: eight more checks added to+/// tools/dsl-live-check.py (T33-T39, T42), all passing on the same install.+pub const LIVE_2026_10_14_B: Measurement = Measurement { year: 2026, major: 34, edition: "connected", date: "2026-10-14", bridge: "0.10.4" };+ const M2026: &[Measurement] = &[LIVE_2026_10_14];+const M2026B: &[Measurement] = &[LIVE_2026_10_14_B]; const UNMEASURED: &[Measurement] = &[];  /// One variant of one DSL op, and where it was measured live.@@ -117,21 +122,29 @@ pub const OP_FACTS: &[OpFact] = &[     OpFact { id: "new_assembly", what: "a new assembly document", checks: "not in the live battery", measured: UNMEASURED },     OpFact { id: "sketch.plane.front", what: "a sketch on Front Plane", checks: "T01 T03 T10 T14 T27", measured: M2026 },     OpFact { id: "sketch.plane.top", what: "a sketch on Top Plane", checks: "T02 T13 T24 T30", measured: M2026 },-    OpFact { id: "sketch.plane.right", what: "a sketch on Right Plane (its axes are the documented convention, not a measurement)", checks: "no live check sketches on Right Plane", measured: UNMEASURED },+    OpFact { id: "sketch.plane.right", what: "a sketch on Right Plane (sketch +x = model −Z, +y = model +Y; a boss goes +X)", checks: "T35", measured: M2026B },     OpFact { id: "sketch.plane.named", what: "a sketch on a named plane other than Front/Top/Right", checks: "not in the live battery", measured: UNMEASURED },     OpFact { id: "sketch.offset.top.positive", what: "a sketch on a plane offset + from Top Plane", checks: "T04 T24", measured: M2026 },     OpFact { id: "sketch.offset.top.negative", what: "a sketch on a plane offset − from Top Plane (keeps Top Plane's axes)", checks: "T05 T26", measured: M2026 },-    OpFact { id: "sketch.offset.other", what: "a sketch on a plane offset from Front Plane, Right Plane or a named plane", checks: "not in the live battery", measured: UNMEASURED },-    OpFact { id: "sketch.face.top", what: "a sketch on a face parallel to Top Plane, picked by a point (the top face, and the bottom face hidden in an iso view)", checks: "T06 T20 T21 T25 T28 T32", measured: M2026 },-    OpFact { id: "sketch.face.side", what: "a sketch on a face parallel to Front or Right Plane", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "sketch.offset.front.positive", what: "a sketch on a plane offset + from Front Plane", checks: "T38", measured: M2026B },+    OpFact { id: "sketch.offset.front.negative", what: "a sketch on a plane offset − from Front Plane", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "sketch.offset.right.positive", what: "a sketch on a plane offset + from Right Plane", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "sketch.offset.right.negative", what: "a sketch on a plane offset − from Right Plane", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "sketch.offset.named", what: "a sketch on a plane offset from a named plane", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "sketch.face.top.positive", what: "a sketch on a face facing +Y (a top face), picked by a point", checks: "T06 T20 T21 T28 T32 T39", measured: M2026 },+    OpFact { id: "sketch.face.top.negative", what: "a sketch on a face facing −Y (a bottom face, hidden in an iso view)", checks: "T25", measured: M2026 },+    OpFact { id: "sketch.face.right.positive", what: "a sketch on a face facing +X (keeps Right Plane's axes, the origin projected)", checks: "T36", measured: M2026B },+    OpFact { id: "sketch.face.right.negative", what: "a sketch on a face facing −X", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "sketch.face.front.negative", what: "a sketch on a face facing −Z (keeps Front Plane's axes, the origin projected)", checks: "T37", measured: M2026B },+    OpFact { id: "sketch.face.front.positive", what: "a sketch on a face facing +Z", checks: "not in the live battery", measured: UNMEASURED },     OpFact { id: "sketch.rect", what: "a rectangle (corner or center anchor)", checks: "T01 T02 T03", measured: M2026 },     OpFact { id: "sketch.circle", what: "a circle", checks: "T04 T06 T30", measured: M2026 },     OpFact { id: "sketch.multiProfile", what: "several profiles in one sketch (a ring, disjoint rectangles, the LEGO tubes)", checks: "T13 T14 T24", measured: M2026 },     OpFact { id: "extrude.one", what: "a boss one way (the plane's normal, or outward from a face)", checks: "T01 T06 T25 and every base block", measured: M2026 },-    OpFact { id: "extrude.one.reversed", what: "a boss whose direction flag is reversed (reverse:true)", checks: "no live boss has run reversed: every one went the default way", measured: UNMEASURED },+    OpFact { id: "extrude.one.reversed", what: "a boss whose direction flag is reversed (reverse:true: from Top Plane it goes −Y)", checks: "T33", measured: M2026B },     OpFact { id: "extrude.both", what: "a boss both ways (depthMm and depth2Mm)", checks: "T10", measured: M2026 },     OpFact { id: "extrude.midPlane", what: "a boss symmetric about the sketch plane", checks: "T11", measured: M2026 },-    OpFact { id: "extrude.one.throughAll", what: "a boss through all, one way", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "extrude.one.throughAll", what: "a boss through all, one way", checks: "T34", measured: M2026B },     OpFact { id: "extrude.both.throughAll", what: "a boss through all, both ways", checks: "not in the live battery", measured: UNMEASURED },     OpFact { id: "cut.one", what: "a cut one way, the default direction (the plane's −normal, into a face)", checks: "T07 (nothing on that side: the step failed, as it must) and T27's first direction", measured: M2026 },     OpFact { id: "cut.one.reversed", what: "a cut one way, reversed (reverse:true)", checks: "T08 T24", measured: M2026 },@@ -149,8 +162,9 @@ pub const OP_FACTS: &[OpFact] = &[     OpFact { id: "chamfer.atPoints", what: "a chamfer on edges picked by points", checks: "T18", measured: M2026 },     OpFact { id: "chamfer.allOfFeature", what: "a chamfer on every edge of a feature", checks: "not in the live battery", measured: UNMEASURED },     OpFact { id: "shell.inward", what: "a shell inward, removing faces picked by points (the top face, and the bottom face hidden in an iso view)", checks: "T19 T29", measured: M2026 },-    OpFact { id: "shell.outward", what: "a shell outward", checks: "not in the live battery", measured: UNMEASURED },+    OpFact { id: "shell.outward", what: "a shell outward (the original block becomes the cavity)", checks: "T42", measured: M2026B },     OpFact { id: "linear_pattern.x", what: "a linear pattern along X, one direction (positive or negative spacing)", checks: "T20 T28", measured: M2026 },+    OpFact { id: "linear_pattern.x+z", what: "a linear pattern in two directions, X and Z (positive and negative spacing)", checks: "T39", measured: M2026B },     OpFact { id: "circular_pattern.y", what: "a circular pattern about Y", checks: "T21", measured: M2026 },     OpFact { id: "set_appearance.part", what: "a colour on the part (or on the one body of a single-body part)", checks: "T15 T24", measured: M2026 },     OpFact { id: "set_appearance.face", what: "a colour on one face picked by a point", checks: "T16", measured: M2026 },@@ -191,6 +205,31 @@ fn axis_name(a: crate::script::Axis) -> &'static str {     } } +/// Which way a picked face faces along its parallel default plane's normal+/// (Front +Z, Top +Y, Right +X): the COM host reports the face's normal, the+/// simulator its `outward` sign.+fn face_side(result: &serde_json::Value, base: &str) -> Option<&'static str> {+    let axis = match base {+        "front" => 2,+        "top" => 1,+        "right" => 0,+        _ => return None,+    };+    let component = result+        .pointer("/resolvedPlane/faceNormal")+        .and_then(|n| n.as_array())+        .and_then(|n| n.get(axis))+        .and_then(|c| c.as_f64())+        .or_else(|| result.pointer("/resolvedPlane/outward").and_then(|o| o.as_f64()))?;+    if component > 0.5 {+        Some("positive")+    } else if component < -0.5 {+        Some("negative")+    } else {+        None+    }+}+ fn plane_key(name: &str) -> &'static str {     match crate::modeling::default_index(name) {         Some(0) => "front",@@ -213,12 +252,15 @@ pub fn op_variants(op: &crate::script::Op, result: &serde_json::Value) -> Vec<St             v.push(match plane {                 PlaneRef::Named(n) => format!("sketch.plane.{}", plane_key(n)),                 PlaneRef::Offset { from, offset } => match plane_key(from) {-                    "top" => format!("sketch.offset.top.{}", if *offset < 0.0 { "negative" } else { "positive" }),-                    _ => "sketch.offset.other".into(),+                    "named" => "sketch.offset.named".into(),+                    base => format!("sketch.offset.{base}.{}", if *offset < 0.0 { "negative" } else { "positive" }),                 },                 PlaneRef::Face { .. } => {                     let from = result.pointer("/resolvedPlane/offsetFrom").and_then(|f| f.as_str()).unwrap_or("");-                    if plane_key(from) == "top" { "sketch.face.top".into() } else { "sketch.face.side".into() }+                    match (plane_key(from), face_side(result, plane_key(from))) {+                        ("named", _) | (_, None) => "sketch.face.unknown".into(),+                        (base, Some(side)) => format!("sketch.face.{base}.{side}"),+                    }                 }             });             if entities.iter().any(|e| matches!(e, crate::script::Entity::Rect { .. })) {@@ -262,7 +304,9 @@ pub fn op_variants(op: &crate::script::Op, result: &serde_json::Value) -> Vec<St         }         Op::Shell { outward, .. } => v.push(if *outward { "shell.outward".into() } else { "shell.inward".into() }),         Op::LinearPattern { dirs, .. } => {-            let axes: Vec<&str> = dirs.iter().map(|d| axis_name(d.0)).collect();+            // The two directions in either order are the same pattern.+            let mut axes: Vec<&str> = dirs.iter().map(|d| axis_name(d.0)).collect();+            axes.sort_unstable();             v.push(format!("linear_pattern.{}", axes.join("+")));         }         Op::CircularPattern { axis, .. } => v.push(format!("circular_pattern.{}", axis_name(*axis))),@@ -312,7 +356,7 @@ pub fn op_variants(op: &crate::script::Op, result: &serde_json::Value) -> Vec<St /// that very release. pub fn proof(variants: &[String], major: Option<u32>) -> serde_json::Value {     let mut unmeasured: Vec<serde_json::Value> = Vec::new();-    let mut common: Option<Vec<Measurement>> = None;+    let mut per_variant: Vec<Vec<Measurement>> = Vec::new();     for id in variants {         let (what, measured): (String, Vec<Measurement>) = match op_fact(id) {             Some(f) => (f.what.to_string(), f.measured.to_vec()),@@ -321,20 +365,34 @@ pub fn proof(variants: &[String], major: Option<u32>) -> serde_json::Value {         if measured.is_empty() {             unmeasured.push(serde_json::json!({"variant": id, "what": what}));         }-        common = Some(match common {-            None => measured,-            Some(c) => c.into_iter().filter(|m| measured.contains(m)).collect(),-        });+        per_variant.push(measured);     }-    let on = common.unwrap_or_default();-    let live = major.map(|m| on.iter().any(|x| x.major == m)).unwrap_or(false);+    // A RELEASE COVERS THE STEP when every variant was measured on it — in the+    // same run or in different ones (a face sketch measured by 0.10.4's checks+    // and the selection rung by 0.10.3's are both measured on 2026).+    let mut majors: Vec<u32> = per_variant.iter().flatten().map(|m| m.major).collect();+    majors.sort_unstable();+    majors.dedup();+    let covered: Vec<u32> = if per_variant.is_empty() {+        Vec::new()+    } else {+        majors.into_iter().filter(|mj| per_variant.iter().all(|ms| ms.iter().any(|m| m.major == *mj))).collect()+    };+    let mut on: Vec<Measurement> = Vec::new();+    for m in per_variant.iter().flatten() {+        if covered.contains(&m.major) && !on.contains(m) {+            on.push(*m);+        }+    }+    let live = major.map(|m| covered.contains(&m)).unwrap_or(false);     let mut out = serde_json::json!({         "live": live,         "measuredOn": on.iter().map(Measurement::to_json).collect::<Vec<_>>(),         "variants": variants,     });     if !live {-        let years: Vec<String> = on.iter().map(|m| m.year.to_string()).collect();+        let mut years: Vec<String> = on.iter().map(|m| m.year.to_string()).collect();+        years.dedup();         out["why"] = serde_json::json!(if !unmeasured.is_empty() {             "not measured live: see `unmeasured` — verify this step's geometry with swx_inspect_bodies".to_string()         } else if on.is_empty() {@@ -428,7 +486,7 @@ mod tests {         // What docs/LIVE-RESULTS.md lists as measured...         for id in [             "new_part", "sketch.plane.front", "sketch.plane.top", "sketch.offset.top.positive", "sketch.offset.top.negative",-            "sketch.face.top", "sketch.rect", "sketch.circle", "sketch.multiProfile", "extrude.one", "extrude.both",+            "sketch.face.top.positive", "sketch.face.top.negative", "sketch.rect", "sketch.circle", "sketch.multiProfile", "extrude.one", "extrude.both",             "extrude.midPlane", "cut.one", "cut.one.reversed", "cut.one.throughAll.reversed", "cut.both", "cut.midPlane",             "set_appearance.part", "set_appearance.face", "set_material", "set_material.reappliesAppearance",             "fillet.allOfFeature", "fillet.FeatureFillet3", "chamfer.atPoints", "shell.inward", "linear_pattern.x",@@ -436,6 +494,17 @@ mod tests {         ] {             assert_eq!(op_fact(id).map(|f| f.measured), Some(M2026), "{id} was measured on 2026");         }+        // ...and the eight checks added with 0.10.4 (T33-T39, T42), measured the same day.+        for id in [+            "extrude.one.reversed", "extrude.one.throughAll", "sketch.plane.right", "sketch.face.right.positive",+            "sketch.face.front.negative", "sketch.offset.front.positive", "linear_pattern.x+z", "shell.outward",+        ] {+            assert_eq!(op_fact(id).map(|f| f.measured), Some(M2026B), "{id} was measured on 2026 by 0.10.4's checks");+        }+        assert_eq!(+            LIVE_2026_10_14_B.to_json(),+            serde_json::json!({"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.4"})+        );         // ...and EXACTLY what is not: its "not yet measured" list (throughAll+both         // on a boss, the ladder's later rungs, the CreateDefinition fillet) plus         // the variants no live check built — a reversed boss among them.@@ -444,10 +513,11 @@ mod tests {         assert_eq!(             unmeasured,             [-                "chamfer.allOfFeature", "extrude.both.throughAll", "extrude.one.reversed", "extrude.one.throughAll",-                "fillet.createDefinition", "new_assembly", "rebuild", "save_as", "select.entitySelect4",-                "select.selectByIdAtPoint", "select.selectByRay", "set_appearance.body", "shell.outward",-                "sketch.face.side", "sketch.offset.other", "sketch.plane.named", "sketch.plane.right", "zoom_fit",+                "chamfer.allOfFeature", "extrude.both.throughAll", "fillet.createDefinition", "new_assembly", "rebuild",+                "save_as", "select.entitySelect4", "select.selectByIdAtPoint", "select.selectByRay", "set_appearance.body",+                "sketch.face.front.positive", "sketch.face.right.negative", "sketch.offset.front.negative",+                "sketch.offset.named", "sketch.offset.right.negative", "sketch.offset.right.positive", "sketch.plane.named",+                "zoom_fit",             ]         );     }@@ -484,13 +554,20 @@ mod tests {         assert_eq!(ids(&o[2], serde_json::json!({"dir": {"value": true}})), ["extrude.one.reversed"], "a face's flip is the reversed call too");         assert_eq!(ids(&o[5], serde_json::json!({})), ["sketch.offset.top.negative", "sketch.circle"]);         assert_eq!(ids(&o[6], serde_json::json!({"dir": {"value": true}})), ["cut.one.throughAll.reversed"]);-        assert_eq!(ids(&o[7], serde_json::json!({})), ["sketch.offset.other", "sketch.circle"]);+        assert_eq!(ids(&o[7], serde_json::json!({})), ["sketch.offset.front.positive", "sketch.circle"]);         assert_eq!(ids(&o[8], serde_json::json!({})), ["extrude.both.throughAll"]);         assert_eq!(-            ids(&o[9], serde_json::json!({"resolvedPlane": {"offsetFrom": "Top Plane", "selectedVia": "MultiSelect2(typed array)"}})),-            ["sketch.face.top", "sketch.circle", "select.multiSelectTyped"]+            ids(&o[9], serde_json::json!({"resolvedPlane": {"offsetFrom": "Top Plane", "faceNormal": [0.0, 1.0, 0.0], "selectedVia": "MultiSelect2(typed array)"}})),+            ["sketch.face.top.positive", "sketch.circle", "select.multiSelectTyped"]         );-        assert_eq!(ids(&o[9], serde_json::json!({"resolvedPlane": {"offsetFrom": "Front Plane"}}))[0], "sketch.face.side");+        // The face's own normal (COM) or its outward sign (the simulator) says which side.+        let face = |from: &str, n: serde_json::Value| ids(&o[9], serde_json::json!({"resolvedPlane": {"offsetFrom": from, "faceNormal": n}}))[0].clone();+        assert_eq!(face("Top Plane", serde_json::json!([0.0, -1.0, 0.0])), "sketch.face.top.negative");+        assert_eq!(face("Right Plane", serde_json::json!([1.0, -0.0, -0.0])), "sketch.face.right.positive", "T36");+        assert_eq!(face("Front Plane", serde_json::json!([-0.0, -0.0, -1.0])), "sketch.face.front.negative", "T37");+        assert_eq!(face("Front Plane", serde_json::json!([0.0, 0.0, 1.0])), "sketch.face.front.positive");+        assert_eq!(ids(&o[9], serde_json::json!({"resolvedPlane": {"offsetFrom": "Right Plane", "outward": -1}}))[0], "sketch.face.right.negative");+        assert_eq!(face("Top Plane", serde_json::json!(null)), "sketch.face.unknown", "no side, no claim");         assert_eq!(             ids(&o[10], serde_json::json!({"route": "FeatureFillet3", "edges": {"selectedVia": "MultiSelect2(typed array)"}})),             ["fillet.allOfFeature", "fillet.FeatureFillet3", "select.multiSelectTyped"]@@ -502,6 +579,8 @@ mod tests {         );         assert_eq!(ids(&o[12], serde_json::json!({"selectedVia": "SelectByID2 at the point (view-dependent)"})), ["shell.outward", "select.selectByIdAtPoint"]);         assert_eq!(ids(&o[13], serde_json::json!({})), ["linear_pattern.x+z"]);+        let zx = ops(serde_json::json!([{"op": "linear_pattern", "feature": "last", "directions": [{"axis": "z", "count": 2, "spacingMm": 5}, {"axis": "x", "count": 2, "spacingMm": 5}]}]));+        assert_eq!(ids(&zx[0], serde_json::json!({})), ["linear_pattern.x+z"], "either order is the same pattern");         assert_eq!(ids(&o[14], serde_json::json!({})), ["circular_pattern.y"]);         assert_eq!(ids(&o[15], serde_json::json!({"route": "part (single-body part)"})), ["set_appearance.part"]);         assert_eq!(ids(&o[15], serde_json::json!({"route": "body Boss-Extrude1"})), ["set_appearance.body"]);@@ -513,12 +592,12 @@ mod tests {     fn proven_is_live_only_where_every_variant_was_measured_on_this_release() {         let v = |ids: &[&str]| ids.iter().map(|s| s.to_string()).collect::<Vec<_>>();         // The coordinator's shape, on SOLIDWORKS 2026.-        let p = proof(&v(&["sketch.face.top", "sketch.circle", "select.multiSelectTyped"]), Some(34));+        let p = proof(&v(&["sketch.face.top.positive", "sketch.circle", "select.multiSelectTyped"]), Some(34));         assert_eq!(p["live"], true);         assert_eq!(p["measuredOn"], serde_json::json!([LIVE_2026_10_14.to_json()]));         assert!(p.get("why").is_none() && p.get("unmeasured").is_none());         // The same step on SOLIDWORKS 2021: measured elsewhere, not live here.-        let p = proof(&v(&["sketch.face.top", "sketch.circle"]), Some(29));+        let p = proof(&v(&["sketch.face.top.positive", "sketch.circle"]), Some(29));         assert_eq!(p["live"], false);         assert_eq!(p["measuredOn"].as_array().map(|a| a.len()), Some(1));         assert!(p["why"].as_str().unwrap_or("").contains("this is SOLIDWORKS 2021"), "{p}");@@ -529,8 +608,13 @@ mod tests {         assert_eq!(p["live"], false);         assert_eq!(p["measuredOn"], serde_json::json!([]));         assert_eq!(p["unmeasured"][0]["variant"], "fillet.createDefinition");+        // Variants measured in DIFFERENT runs on the same release cover the step,+        // and both runs are listed.+        let p = proof(&v(&["sketch.face.right.positive", "sketch.circle", "select.multiSelectTyped"]), Some(34));+        assert_eq!(p["live"], true, "{p}");+        assert_eq!(p["measuredOn"], serde_json::json!([LIVE_2026_10_14_B.to_json(), LIVE_2026_10_14.to_json()]));         // A variant the table does not know has never been built live.-        let p = proof(&v(&["linear_pattern.x+z"]), Some(34));+        let p = proof(&v(&["linear_pattern.x+y"]), Some(34));         assert_eq!(p["live"], false);         assert!(p["unmeasured"][0]["what"].as_str().unwrap_or("").contains("no live check"));         // The table, as describe and selftest report it.
src/launch.rs+8
@@ -3001,6 +3001,14 @@ pub fn note_attached_solidworks(pid: Option<u32>) {     ATTACHED_SW_PID.store(pid.unwrap_or(0), Ordering::SeqCst); } +/// The SOLIDWORKS pid COM last reported being attached to (swx_status), if any.+pub fn attached_solidworks_pid() -> Option<u32> {+    match ATTACHED_SW_PID.load(Ordering::SeqCst) {+        0 => None,+        p => Some(p),+    }+}+ fn reattr_refuse(reason: &str) -> Option<String> {     *REATTR_REASON.lock().unwrap_or_else(|e| e.into_inner()) = Some(reason.to_string());     None
src/launchos.rs+171
@@ -415,6 +415,30 @@ pub struct ProcMetrics {     pub handles: Option<u32>, } +/// THE MACHINE'S MEMORY (0.10.5), from GlobalMemoryStatusEx. The COMMIT pair+/// is what runs out: measured 2026-10-14, the SOLIDWORKS Resource Monitor+/// warned "available committed memory is critically low" at 72.3 of 76.6 GB+/// committed while gigabytes of RAM were still free.+#[derive(Clone, Debug, Default, PartialEq)]+pub struct SystemMemory {+    /// `ullTotalPageFile` — the commit limit (RAM plus the page file).+    pub commit_limit_mb: u64,+    /// `ullAvailPageFile` — what can still be committed.+    pub commit_avail_mb: u64,+    pub phys_total_mb: u64,+    pub phys_avail_mb: u64,+}++/// One process's committed bytes (`PrivateUsage`). READ-ONLY FACTS: the bridge+/// reports who holds the memory and never acts on a process that is not+/// SOLIDWORKS.+#[derive(Clone, Debug, PartialEq)]+pub struct ProcPrivate {+    pub name: String,+    pub pid: u32,+    pub private_mb: u64,+}+ /// The OS facts, behind one trait so the state machine has exactly one seam and /// the suite can drive every branch of it. pub trait LaunchOs: Send + Sync {@@ -516,6 +540,23 @@ pub trait LaunchOs: Send + Sync {     fn now(&self) -> u64 {         epoch_secs()     }+    /// THE COMMIT CHARGE (0.10.5). `Err` = could not read, which is UNKNOWN —+    /// never "plenty".+    fn system_memory(&self) -> Result<SystemMemory, String> {+        Err("this host cannot read the machine's memory".into())+    }+    /// EVERY process this host can open, with its committed bytes — for the+    /// largest-holders report. Read-only; nothing here ever acts on one.+    fn private_by_process(&self) -> Result<Vec<ProcPrivate>, String> {+        Err("this host cannot list processes by memory".into())+    }+    /// One process's committed MB and handle count, read WITHOUT side effects+    /// (the simulator's `process_metrics` advances its CPU clock; this must not,+    /// because it runs on every ledger line).+    fn memory_of(&self, pid: u32) -> (Option<u64>, Option<u32>) {+        let m = self.process_metrics(pid);+        (m.private_mb, m.handles)+    }     /// Name for the ledger, so a reader can tell a simulated run from a real one.     fn kind(&self) -> &'static str; }@@ -1419,11 +1460,84 @@ impl LaunchOs for FakeOs {         }     } +    /// `memory.json` — `{"commitLimitMB", "commitAvailMB", "physTotalMB",+    /// "physAvailMB", "processes": [{"name", "pid", "privateMB"}], "afterQuit":+    /// {...}}`. `afterQuit` overlays the rest once a quit has happened, so a+    /// scenario can show what a recycle returned.+    fn system_memory(&self) -> Result<SystemMemory, String> {+        let m = self.memory_json()?;+        let get = |k: &str| m.get(k).and_then(|v| v.as_u64());+        Ok(SystemMemory {+            commit_limit_mb: get("commitLimitMB").ok_or("simulated: memory.json has no commitLimitMB")?,+            commit_avail_mb: get("commitAvailMB").ok_or("simulated: memory.json has no commitAvailMB")?,+            phys_total_mb: get("physTotalMB").unwrap_or(0),+            phys_avail_mb: get("physAvailMB").unwrap_or(0),+        })+    }++    fn private_by_process(&self) -> Result<Vec<ProcPrivate>, String> {+        let m = self.memory_json()?;+        let mut out: Vec<ProcPrivate> = m+            .get("processes")+            .and_then(|p| p.as_array())+            .map(|a| {+                a.iter()+                    .filter_map(|r| {+                        Some(ProcPrivate {+                            name: r.get("name")?.as_str()?.to_string(),+                            pid: r.get("pid")?.as_u64()? as u32,+                            private_mb: r.get("privateMB")?.as_u64()?,+                        })+                    })+                    .collect()+            })+            .unwrap_or_default();+        // The simulated SOLIDWORKS, holding what sldworks.json says.+        if let Ok(census) = self.processes() {+            for p in census.iter().filter(|p| p.is("SLDWORKS.exe")) {+                if !out.iter().any(|x| x.pid == p.pid) {+                    if let (Some(mb), _) = self.memory_of(p.pid) {+                        out.push(ProcPrivate { name: p.image.clone(), pid: p.pid, private_mb: mb });+                    }+                }+            }+        }+        Ok(out)+    }++    fn memory_of(&self, _pid: u32) -> (Option<u64>, Option<u32>) {+        let s = self.read_json("sldworks.json");+        (+            s.get("privateMB").and_then(|v| v.as_u64()).or(Some(800)),+            s.get("handles").and_then(|v| v.as_u64()).map(|h| h as u32).or(Some(3000)),+        )+    }+     fn kind(&self) -> &'static str {         "simulated"     } } +impl FakeOs {+    fn memory_json(&self) -> Result<Value, String> {+        let mut m = self.read_json("memory.json");+        if !m.is_object() {+            return Err("simulated: no memory.json".into());+        }+        let quit = std::fs::read_to_string(self.dir.join("quit.jsonl")).map(|t| !t.trim().is_empty()).unwrap_or(false);+        if quit {+            if let Some(Value::Object(after)) = m.get("afterQuit").cloned() {+                if let Value::Object(map) = &mut m {+                    for (k, v) in after {+                        map.insert(k, v);+                    }+                }+            }+        }+        Ok(m)+    }+}+ // ── the real one ────────────────────────────────────────────────────────────  #[cfg(windows)]@@ -1607,6 +1721,63 @@ mod win {             }         } +        fn system_memory(&self) -> Result<SystemMemory, String> {+            use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};+            const MB: u64 = 1024 * 1024;+            let mut m = MEMORYSTATUSEX { dwLength: std::mem::size_of::<MEMORYSTATUSEX>() as u32, ..Default::default() };+            unsafe { GlobalMemoryStatusEx(&mut m) }.map_err(|e| format!("GlobalMemoryStatusEx failed: {e}"))?;+            Ok(SystemMemory {+                commit_limit_mb: m.ullTotalPageFile / MB,+                commit_avail_mb: m.ullAvailPageFile / MB,+                phys_total_mb: m.ullTotalPhys / MB,+                phys_avail_mb: m.ullAvailPhys / MB,+            })+        }++        /// Every process this account can open (PROCESS_QUERY_LIMITED_INFORMATION),+        /// with its PrivateUsage. A protected process that cannot be opened is+        /// simply absent — this is a report of the largest holders, not a census+        /// anything acts on.+        fn private_by_process(&self) -> Result<Vec<ProcPrivate>, String> {+            const MB: u64 = 1024 * 1024;+            unsafe {+                let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)+                    .map_err(|e| format!("CreateToolhelp32Snapshot failed: {e}"))?;+                let mut entry = PROCESSENTRY32W {+                    dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,+                    ..Default::default()+                };+                let mut out = Vec::new();+                if Process32FirstW(snap, &mut entry).is_ok() {+                    loop {+                        let pid = entry.th32ProcessID;+                        if pid != 0 {+                            let len = entry.szExeFile.iter().position(|c| *c == 0).unwrap_or(entry.szExeFile.len());+                            let name = String::from_utf16_lossy(&entry.szExeFile[..len]);+                            if let Ok(h) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {+                                let mut c = PROCESS_MEMORY_COUNTERS_EX::default();+                                if K32GetProcessMemoryInfo(+                                    h,+                                    &mut c as *mut _ as *mut PROCESS_MEMORY_COUNTERS,+                                    std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,+                                )+                                .as_bool()+                                {+                                    out.push(ProcPrivate { name, pid, private_mb: c.PrivateUsage as u64 / MB });+                                }+                                let _ = CloseHandle(h);+                            }+                        }+                        if Process32NextW(snap, &mut entry).is_err() {+                            break;+                        }+                    }+                }+                let _ = CloseHandle(snap);+                Ok(out)+            }+        }+         fn launcher_logs(&self) -> Result<Vec<LogFile>, String> {             logs_in("SWXDesktopLauncher")         }
src/ledger.rs+6−1
@@ -104,7 +104,12 @@ pub fn record_in(root: &std::path::Path, action: &str, detail: Value) {             );         }     }-    let entry = json!({"at": now_epoch(), "seq": next(), "action": action, "detail": detail});+    let mut entry = json!({"at": now_epoch(), "seq": next(), "action": action, "detail": detail});+    // A MEMORY SAMPLE ON EVERY LINE (0.10.5): commit available and SOLIDWORKS's+    // own share, so growth over a long run is visible after the fact.+    if let Some(mem) = crate::memory::ledger_sample() {+        entry["mem"] = mem;+    }     lines.push(entry.to_string());     // ATOMIC. The ledger is the one artefact that outlives the bridge — it is     // the record of what this bridge did on somebody's machine while they were
src/lib.rs+1
@@ -39,6 +39,7 @@ pub mod launch; pub mod launchos; pub mod modeling; pub mod ledger;+pub mod memory; pub mod owned; pub mod perflog; pub mod presence;
src/memory.rsadded+752
@@ -0,0 +1,752 @@+//! MEMORY (0.10.5) — the commit charge, SOLIDWORKS's share of it, and who else+//! holds it.+//!+//! MEASURED 2026-10-14 on adom_drew2 (31.4 GB RAM, an automatic 46 GB page+//! file): the SOLIDWORKS Resource Monitor warned "Available committed memory is+//! critically low" at 72.3 of 76.6 GB committed (94 %), 4.2 GB available. The+//! holders were Fusion 360 (20.7 GB, the user's, two days old), the+//! bridge-started SOLIDWORKS (10.6 GB with ZERO documents open, after about a+//! hundred scratch parts were built and closed in it — SOLIDWORKS does not give+//! memory back when a document closes), Chrome (10.2 GB over 51 processes) and+//! WSL (4.7 GB). Quitting that SOLIDWORKS (swx_stop, rung a, 14 s) returned about+//! 16 GB of commit.+//!+//! So this module:+//!   * REPORTS — `swx_status.memory`, and a sentence naming the largest holders+//!     in every reply's `_hint` while pressure is not ok;+//!   * GUARDS — verbs that create or load geometry are refused while commit is+//!     critical (`low_commit_memory`), changing nothing, with the remedy that is+//!     actually safe;+//!   * lets LONG RUNS RECYCLE a session the bridge started (the self-test) —+//!     never one the user is working in, and never any other application.+//!+//! Everything about other processes is READ-ONLY. Nothing here acts on any+//! process; the only action anywhere in this file's orbit is `swx_stop` on the+//! bridge's own, empty SOLIDWORKS session, and that verb keeps every one of its+//! own refusals.++use serde_json::{json, Value};++use crate::launchos::{LaunchOs, ProcPrivate, SystemMemory};++/// SOLIDWORKS's own committed memory above which a long run recycles a session+/// the bridge started (6 GB: a sensible budget on a 32 GB PC).+pub const DEFAULT_BUDGET_MB: u64 = 6144;+pub const ENV_BUDGET: &str = "SWX_SW_MEMORY_BUDGET_MB";+/// CRITICAL: commit available under 8 % of the limit, or under 4 GB.+pub const CRITICAL_FRACTION: f64 = 0.08;+pub const CRITICAL_FLOOR_MB: u64 = 4096;+/// HIGH: commit available under 15 % of the limit (or SOLIDWORKS over budget).+pub const HIGH_FRACTION: f64 = 0.15;+/// How many holders a report names.+pub const TOP_N: usize = 5;+/// A recycle is worth its minutes only when SOLIDWORKS's own share is at least+/// this much (or over budget); below it the memory is held elsewhere and a+/// restart would not relieve anything.+pub const RECYCLE_WORTH_MB: u64 = 2048;++pub const READ_ONLY_NOTE: &str = "Facts, read-only. The bridge never closes, stops or kills a \+     process that is not SOLIDWORKS, whatever it holds — report the largest holders to the user \+     and let them decide. The only memory the bridge returns itself is a SOLIDWORKS session it \+     started, with no documents open (swx_stop, then swx_start).";++/// The session budget: `SWX_SW_MEMORY_BUDGET_MB`, else 6144.+pub fn budget() -> (u64, &'static str) {+    match std::env::var(ENV_BUDGET).ok().and_then(|v| v.trim().parse::<u64>().ok()).filter(|v| *v >= 256) {+        Some(v) => (v, ENV_BUDGET),+        None => (DEFAULT_BUDGET_MB, "default"),+    }+}++#[derive(Clone, Copy, Debug, PartialEq, Eq)]+pub enum Pressure {+    Ok,+    High,+    Critical,+    /// The commit charge could not be read. Never reported as "ok".+    Unknown,+}++impl Pressure {+    pub fn name(self) -> &'static str {+        match self {+            Pressure::Ok => "ok",+            Pressure::High => "high",+            Pressure::Critical => "critical",+            Pressure::Unknown => "unknown",+        }+    }+    pub fn pressing(self) -> bool {+        matches!(self, Pressure::High | Pressure::Critical)+    }+}++pub fn gb(mb: u64) -> String {+    format!("{:.1} GB", mb as f64 / 1024.0)+}++/// THE RULE. Critical: commit available under 8 % of the limit or under 4 GB.+/// High: under 15 %, or SOLIDWORKS over its session budget. A commit charge+/// that cannot be read is Unknown (or High, if SOLIDWORKS alone is over budget).+pub fn pressure(sys: Option<&SystemMemory>, sw_private_mb: Option<u64>, budget_mb: u64) -> (Pressure, Vec<String>) {+    let over = sw_private_mb.filter(|p| *p > budget_mb);+    let over_why = |p: u64| format!("SOLIDWORKS holds {} — over its {} session budget", gb(p), gb(budget_mb));+    let Some(s) = sys.filter(|s| s.commit_limit_mb > 0) else {+        return match over {+            Some(p) => (Pressure::High, vec![over_why(p), "the commit charge could not be read".into()]),+            None => (Pressure::Unknown, vec!["the commit charge could not be read".into()]),+        };+    };+    let avail = s.commit_avail_mb as f64;+    let limit = s.commit_limit_mb as f64;+    let mut why = Vec::new();+    if avail < CRITICAL_FRACTION * limit {+        why.push(format!("{} of commit available is under 8 % of the {} limit", gb(s.commit_avail_mb), gb(s.commit_limit_mb)));+    }+    if s.commit_avail_mb < CRITICAL_FLOOR_MB {+        why.push(format!("{} of commit available is under 4 GB", gb(s.commit_avail_mb)));+    }+    if !why.is_empty() {+        if let Some(p) = over {+            why.push(over_why(p));+        }+        return (Pressure::Critical, why);+    }+    if avail < HIGH_FRACTION * limit {+        why.push(format!("{} of commit available is under 15 % of the {} limit", gb(s.commit_avail_mb), gb(s.commit_limit_mb)));+    }+    if let Some(p) = over {+        why.push(over_why(p));+    }+    if why.is_empty() {+        (Pressure::Ok, why)+    } else {+        (Pressure::High, why)+    }+}++/// The largest single processes.+pub fn top_processes(list: &[ProcPrivate], n: usize) -> Vec<ProcPrivate> {+    let mut v = list.to_vec();+    v.sort_by(|a, b| b.private_mb.cmp(&a.private_mb).then(a.pid.cmp(&b.pid)));+    v.truncate(n);+    v+}++/// One application's total (Chrome is 51 processes).+#[derive(Clone, Debug, PartialEq)]+pub struct ImageTotal {+    pub name: String,+    pub processes: usize,+    pub private_mb: u64,+}++/// The largest applications, by image name (case-insensitive).+pub fn top_images(list: &[ProcPrivate], n: usize) -> Vec<ImageTotal> {+    let mut totals: Vec<ImageTotal> = Vec::new();+    for p in list {+        match totals.iter_mut().find(|t| t.name.eq_ignore_ascii_case(&p.name)) {+            Some(t) => {+                t.processes += 1;+                t.private_mb += p.private_mb;+            }+            None => totals.push(ImageTotal { name: p.name.clone(), processes: 1, private_mb: p.private_mb }),+        }+    }+    totals.sort_by(|a, b| b.private_mb.cmp(&a.private_mb).then(a.name.cmp(&b.name)));+    totals.truncate(n);+    totals+}++/// "Fusion360.exe 20.7 GB, SLDWORKS.exe 10.6 GB, chrome.exe 10.2 GB (51 processes)".+pub fn holders_sentence(images: &[ImageTotal]) -> String {+    images+        .iter()+        .map(|i| {+            if i.processes > 1 {+                format!("{} {} ({} processes)", i.name, gb(i.private_mb), i.processes)+            } else {+                format!("{} {}", i.name, gb(i.private_mb))+            }+        })+        .collect::<Vec<_>>()+        .join(", ")+}++/// One cheap reading: the commit pair and SOLIDWORKS's own share.+#[derive(Clone, Debug)]+pub struct Sample {+    pub sys: Result<SystemMemory, String>,+    pub sw_pid: Option<u32>,+    pub sw_private_mb: Option<u64>,+    pub sw_handles: Option<u32>,+    pub budget_mb: u64,+    pub budget_source: &'static str,+}++impl Sample {+    pub fn pressure(&self) -> (Pressure, Vec<String>) {+        pressure(self.sys.as_ref().ok(), self.sw_private_mb, self.budget_mb)+    }+    pub fn commit_avail_mb(&self) -> Option<u64> {+        self.sys.as_ref().ok().map(|s| s.commit_avail_mb)+    }+}++/// The SOLIDWORKS whose memory is reported: the one COM is attached to, when it+/// is in the census; else the newest sldworks.exe.+pub fn solidworks_pid(os: &dyn LaunchOs) -> Option<u32> {+    let census = os.processes().ok()?;+    let sw: Vec<_> = census.iter().filter(|p| p.is("SLDWORKS.exe")).collect();+    if let Some(a) = crate::launch::attached_solidworks_pid() {+        if sw.iter().any(|p| p.pid == a) {+            return Some(a);+        }+    }+    sw.iter().max_by_key(|p| p.created_at.unwrap_or(0)).map(|p| p.pid)+}++pub fn sample(os: &dyn LaunchOs) -> Sample {+    let (budget_mb, budget_source) = budget();+    let sw_pid = solidworks_pid(os);+    let (sw_private_mb, sw_handles) = sw_pid.map(|p| os.memory_of(p)).unwrap_or((None, None));+    Sample { sys: os.system_memory(), sw_pid, sw_private_mb, sw_handles, budget_mb, budget_source }+}++/// The `memory` block.+pub fn report_of(s: &Sample, procs: Result<Vec<ProcPrivate>, String>) -> Value {+    let (p, why) = s.pressure();+    let mut v = json!({+        "pressure": p.name(),+        "pressureWhy": why,+        "sessionBudgetMb": s.budget_mb,+        "sessionBudgetSource": s.budget_source,+        "solidworks": s.sw_pid.map(|pid| json!({+            "pid": pid,+            "privateMb": s.sw_private_mb,+            "handles": s.sw_handles,+            "overBudget": s.sw_private_mb.map(|m| m > s.budget_mb),+        })),+        "readOnly": READ_ONLY_NOTE,+        "rule": "critical: commit available under 8 % of the limit or under 4 GB; high: under 15 %, \+                 or SOLIDWORKS over sessionBudgetMb (SWX_SW_MEMORY_BUDGET_MB). Verbs that create or \+                 load geometry are refused at critical (low_commit_memory).",+    });+    if let Value::Object(m) = &mut v {+        match &s.sys {+            Ok(sys) => {+                let used = sys.commit_limit_mb.saturating_sub(sys.commit_avail_mb);+                m.insert("commitUsedMb".into(), json!(used));+                m.insert("commitLimitMb".into(), json!(sys.commit_limit_mb));+                m.insert("commitAvailableMb".into(), json!(sys.commit_avail_mb));+                m.insert(+                    "commitUsedPercent".into(),+                    json!(if sys.commit_limit_mb > 0 { (used as f64 * 1000.0 / sys.commit_limit_mb as f64).round() / 10.0 } else { 0.0 }),+                );+                m.insert("physicalTotalMb".into(), json!(sys.phys_total_mb));+                m.insert("physicalAvailableMb".into(), json!(sys.phys_avail_mb));+            }+            Err(e) => {+                m.insert("error".into(), json!(e));+            }+        }+        match procs {+            Ok(list) => {+                let images = top_images(&list, TOP_N);+                m.insert(+                    "topProcesses".into(),+                    json!(top_processes(&list, TOP_N)+                        .iter()+                        .map(|p| json!({"name": p.name, "pid": p.pid, "privateMb": p.private_mb}))+                        .collect::<Vec<_>>()),+                );+                m.insert(+                    "topImages".into(),+                    json!(images.iter().map(|i| json!({"name": i.name, "processes": i.processes, "privateMb": i.private_mb})).collect::<Vec<_>>()),+                );+                m.insert("largestHolders".into(), json!(holders_sentence(&images)));+            }+            Err(e) => {+                m.insert("topProcesses".into(), Value::Null);+                m.insert("topProcessesError".into(), json!(e));+            }+        }+    }+    v+}++pub fn report(os: &dyn LaunchOs) -> Value {+    report_of(&sample(os), os.private_by_process())+}++/// The numbers in one clause: "72.3 GB of 76.6 GB committed (94.4 %), 4.2 GB+/// available".+pub fn commit_clause(report: &Value) -> String {+    match (report["commitUsedMb"].as_u64(), report["commitLimitMb"].as_u64(), report["commitAvailableMb"].as_u64()) {+        (Some(u), Some(l), Some(a)) => format!(+            "{} of {} committed ({} %), {} available",+            gb(u),+            gb(l),+            report["commitUsedPercent"].as_f64().unwrap_or(0.0),+            gb(a)+        ),+        _ => "the commit charge could not be read".to_string(),+    }+}++/// The sentence every reply carries while pressure is high or critical.+pub fn pressure_hint(report: &Value) -> Option<String> {+    let p = report["pressure"].as_str()?;+    if p != "high" && p != "critical" {+        return None;+    }+    let sw = match (report.pointer("/solidworks/privateMb").and_then(|v| v.as_u64()), report["sessionBudgetMb"].as_u64()) {+        (Some(m), Some(b)) => format!(" SOLIDWORKS holds {} (its session budget is {}).", gb(m), gb(b)),+        _ => String::new(),+    };+    let holders = report["largestHolders"].as_str().filter(|h| !h.is_empty()).unwrap_or("could not be read");+    let what = if p == "critical" {+        "Verbs that create or load geometry are refused until it recovers (low_commit_memory)."+    } else {+        "Split or pause long runs."+    };+    Some(format!(+        "MEMORY {}: {}.{sw} Largest holders: {holders}. {what} A SOLIDWORKS session the bridge \+         started, with no documents open, returns its memory with swx_stop {{userAuthorized:true}} \+         then swx_start. NEVER close the user's applications; tell the user. See swx_status.memory \+         and the solidworks-memory skill.",+        p.to_uppercase(),+        commit_clause(report),+    ))+}++/// EVERY REPLY, while memory is pressing: the hint, and the numbers. Cheap when+/// it is not (one GlobalMemoryStatusEx and SOLIDWORKS's own counters); the+/// all-process read happens only when there is something to say.+pub fn annotate(out: &mut Value) {+    let os = crate::launchos::os();+    let s = sample(os.as_ref());+    if !s.pressure().0.pressing() {+        return;+    }+    let report = report_of(&s, os.private_by_process());+    let Some(sentence) = pressure_hint(&report) else { return };+    if let Value::Object(m) = out {+        match m.get_mut("_hint") {+            Some(Value::String(h)) if !h.contains("MEMORY HIGH") && !h.contains("MEMORY CRITICAL") => {+                h.push(' ');+                h.push_str(&sentence);+            }+            Some(Value::String(_)) => {}+            Some(_) => {+                m.insert("memoryHint".into(), json!(sentence));+            }+            None => {+                m.insert("_hint".into(), json!(sentence));+            }+        }+        m.entry("memoryPressure").or_insert(json!({+            "pressure": report["pressure"],+            "commitAvailableMb": report["commitAvailableMb"],+            "commitUsedPercent": report["commitUsedPercent"],+            "solidworksPrivateMb": report.pointer("/solidworks/privateMb").cloned().unwrap_or(Value::Null),+            "largestHolders": report["largestHolders"],+        }));+    }+}++/// The ledger's memory sample: commit available and SOLIDWORKS's share, or+/// nothing when neither can be read.+pub fn ledger_sample() -> Option<Value> {+    let os = crate::launchos::os();+    let s = sample(os.as_ref());+    let avail = s.commit_avail_mb();+    if avail.is_none() && s.sw_private_mb.is_none() {+        return None;+    }+    Some(json!({"commitAvailMb": avail, "swPrivateMb": s.sw_private_mb}))+}++/// The same, for the one log line per verb.+pub fn log_suffix() -> String {+    let os = crate::launchos::os();+    let s = sample(os.as_ref());+    match (s.commit_avail_mb(), s.sw_private_mb) {+        (None, None) => String::new(),+        (a, w) => format!(+            " | mem: commit available {}, SOLIDWORKS {}",+            a.map(gb).unwrap_or_else(|| "unknown".into()),+            w.map(gb).unwrap_or_else(|| "not running".into())+        ),+    }+}++// ── the guard ───────────────────────────────────────────────────────────────++/// Verbs that create or load geometry.+pub fn guarded(command: &str, args: &Value) -> bool {+    match command {+        "swx_new_document" | "swx_open" | "swx_import" | "swx_render_image" | "swx_show_part" | "swx_show_assembly"+        | "swx_show_drawing" => true,+        "swx_selftest" => args.get("level").and_then(|l| l.as_str()) == Some("write"),+        "swx_run_script" => args+            .get("script")+            .and_then(|s| s.as_array())+            .map(|steps| steps.iter().any(|st| matches!(st.get("op").and_then(|o| o.as_str()), Some("new_part" | "new_assembly"))))+            .unwrap_or(false),+        _ => false,+    }+}++/// A caller's override: `ignoreMemoryPressure:true` with a reason of 8+ characters.+pub fn override_reason(args: &Value) -> Result<Option<String>, ()> {+    if args.get("ignoreMemoryPressure").and_then(|v| v.as_bool()) != Some(true) {+        return Ok(None);+    }+    match args.get("reason").and_then(|r| r.as_str()).map(str::trim).filter(|r| r.chars().count() >= 8) {+        Some(r) => Ok(Some(r.to_string())),+        None => Err(()),+    }+}++/// What the guard does with a pressure and an override.+#[derive(Clone, Debug, PartialEq)]+pub enum GuardDecision {+    Proceed,+    /// Critical, run anyway: the caller said so, with a reason (ledgered).+    Overridden(String),+    Refuse { override_without_reason: bool },+}++pub fn guard_decision(p: Pressure, over: Result<Option<String>, ()>) -> GuardDecision {+    if p != Pressure::Critical {+        return GuardDecision::Proceed;+    }+    match over {+        Ok(Some(r)) => GuardDecision::Overridden(r),+        Ok(None) => GuardDecision::Refuse { override_without_reason: false },+        Err(()) => GuardDecision::Refuse { override_without_reason: true },+    }+}++/// Whose SOLIDWORKS is this, and is anything open in it?+#[derive(Clone, Debug, Default, PartialEq)]+pub struct SessionFacts {+    pub com_pid: Option<u32>,+    pub open_docs: Option<u64>,+    /// `None` = cannot tell, which is never read as "the bridge's".+    pub bridge_started: Option<bool>,+}++/// Pure: the launch record's claim against the session COM answers for. Both+/// pid AND creation time, like the quit ladder — a pid alone is reusable.+pub fn bridge_started(claim: Option<(u32, u64)>, com_pid: Option<u32>, created: Option<u64>) -> Option<bool> {+    match (com_pid, created) {+        (Some(pid), Some(c)) => Some(claim == Some((pid, c))),+        _ => None,+    }+}++pub fn session_facts(ctx: &crate::verbs::Ctx, os: &dyn LaunchOs) -> SessionFacts {+    let Ok(state) = ctx.call("app_state", json!({}), 20) else { return SessionFacts::default() };+    if state.get("running").and_then(|v| v.as_bool()) == Some(false) {+        return SessionFacts::default();+    }+    let com_pid = state.get("processId").and_then(|v| v.as_u64()).map(|p| p as u32);+    let open_docs = state.get("documentCount").and_then(|v| v.as_u64());+    let created = com_pid.and_then(|pid| os.processes().ok()?.iter().find(|p| p.pid == pid).and_then(|p| p.created_at));+    SessionFacts { com_pid, open_docs, bridge_started: bridge_started(crate::launch::attributed_solidworks(), com_pid, created) }+}++/// THE SAFE REMEDY, and only the safe one.+pub fn remedy(bridge_started: Option<bool>, open_docs: Option<u64>, sw_private_mb: Option<u64>) -> String {+    match (bridge_started, open_docs) {+        (Some(true), Some(0)) => format!(+            "This SOLIDWORKS session was started by the bridge and has no documents open: \+             swx_stop {{\"userAuthorized\": true, \"reason\": \"...\"}} then swx_start frees about {} \+             (SOLIDWORKS keeps memory after its documents close).",+            sw_private_mb.map(gb).unwrap_or_else(|| "its memory".into())+        ),+        (Some(true), Some(n)) => format!(+            "This SOLIDWORKS session was started by the bridge but has {n} document(s) open. Close \+             the bridge's own (swx_reset_workspace); once none are open, swx_stop \+             {{\"userAuthorized\": true}} then swx_start returns its memory. A document the user \+             opened is theirs: tell the user which applications hold the most, and never close \+             them."+        ),+        _ => "Tell the user which applications hold the most memory (largestHolders) and let \+              them decide. Never close them — not their SOLIDWORKS, not Fusion, not the browser, \+              nothing."+            .to_string(),+    }+}++/// The guard, for the dispatcher. `Ok(Some(override))` runs the verb and says it+/// was overridden; `Err(refusal)` changes nothing.+pub fn guard(ctx: &crate::verbs::Ctx, command: &str, args: &Value) -> Result<Option<Value>, Value> {+    let os = crate::launchos::os();+    let s = sample(os.as_ref());+    let (p, why) = s.pressure();+    let decision = guard_decision(p, override_reason(args));+    if decision == GuardDecision::Proceed {+        return Ok(None);+    }+    let report = report_of(&s, os.private_by_process());+    if let GuardDecision::Overridden(reason) = &decision {+        crate::ledger::record(+            "memory_guard_overridden",+            json!({"verb": command, "reason": reason, "pressure": p.name(), "why": why,+                   "commitAvailableMb": report["commitAvailableMb"], "commitLimitMb": report["commitLimitMb"],+                   "solidworksPrivateMb": s.sw_private_mb}),+        );+        return Ok(Some(json!({"overridden": true, "reason": reason, "pressure": p.name(), "commit": commit_clause(&report)})));+    }+    let facts = session_facts(ctx, os.as_ref());+    let fix = remedy(facts.bridge_started, facts.open_docs, s.sw_private_mb);+    crate::ledger::record(+        "memory_guard_refused",+        json!({"verb": command, "why": why, "commitAvailableMb": report["commitAvailableMb"],+               "commitLimitMb": report["commitLimitMb"], "solidworksPrivateMb": s.sw_private_mb}),+    );+    let without_reason = matches!(decision, GuardDecision::Refuse { override_without_reason: true });+    let mut out = crate::verbs::err_code(+        "low_commit_memory",+        format!(+            "{command} refused: available committed memory is critically low — {}. Creating or \+             loading geometry now risks allocation failures in every application on this PC. \+             Nothing was changed.{}",+            commit_clause(&report),+            if without_reason { " (ignoreMemoryPressure needs a reason of 8+ characters.)" } else { "" }+        ),+        format!(+            "Largest holders: {}. {fix} To run it anyway, pass ignoreMemoryPressure:true with a \+             reason; the override is ledgered. See the solidworks-memory skill.",+            report["largestHolders"].as_str().unwrap_or("could not be read")+        ),+    );+    if let Value::Object(m) = &mut out {+        m.insert("memory".into(), report.clone());+        m.insert("largestHolders".into(), report["largestHolders"].clone());+        m.insert("remedy".into(), json!(fix));+        m.insert(+            "solidworksSession".into(),+            json!({"pid": facts.com_pid, "bridgeStarted": facts.bridge_started, "openDocuments": facts.open_docs}),+        );+        m.insert("changedNothing".into(), json!(true));+    }+    Err(out)+}++// ── long runs ───────────────────────────────────────────────────────────────++/// Between two checks of a long run.+#[derive(Clone, Debug, PartialEq)]+pub enum Between {+    Continue,+    Recycle(String),+    Stop(String),+}++/// Does this reading call for a decision at all? (Cheap; the session facts are+/// read only when it does.)+pub fn needs_attention(p: Pressure, sw_private_mb: Option<u64>, budget_mb: u64) -> bool {+    p.pressing() || sw_private_mb.map(|m| m > budget_mb).unwrap_or(false)+}++/// RECYCLE ONLY A SESSION THE BRIDGE STARTED, WITH NOTHING OPEN. Anything else —+/// the user's session, documents open, or a session nobody can identify —+/// STOPS the run instead.+pub fn between_checks(p: Pressure, sw_private_mb: Option<u64>, budget_mb: u64, bridge_started: Option<bool>, open_docs: Option<u64>) -> Between {+    if !needs_attention(p, sw_private_mb, budget_mb) {+        return Between::Continue;+    }+    let mut why: Vec<String> = Vec::new();+    if let Some(m) = sw_private_mb.filter(|m| *m > budget_mb) {+        why.push(format!("SOLIDWORKS holds {} (budget {})", gb(m), gb(budget_mb)));+    }+    if p.pressing() {+        why.push(format!("memory pressure is {}", p.name()));+    }+    let why = why.join("; ");+    let worth = sw_private_mb.map(|m| m > budget_mb || m >= RECYCLE_WORTH_MB).unwrap_or(false);+    match (bridge_started, open_docs) {+        (Some(true), Some(0)) if worth => Between::Recycle(why),+        (Some(true), Some(0)) => Between::Stop(format!(+            "{why}; SOLIDWORKS itself holds only {} — the memory is held by other applications \+             (largestHolders), so recycling SOLIDWORKS would not relieve it",+            sw_private_mb.map(gb).unwrap_or_else(|| "an unknown amount".into())+        )),+        (Some(true), Some(n)) => Between::Stop(format!(+            "{why}; the session is the bridge's but {n} document(s) are open, so it cannot be recycled safely"+        )),+        (Some(true), None) => Between::Stop(format!("{why}; the open-document count could not be read, so it cannot be recycled safely")),+        (Some(false), _) => Between::Stop(format!("{why}; this SOLIDWORKS was not started by the bridge, so the bridge will not end it")),+        (None, _) => Between::Stop(format!("{why}; whose SOLIDWORKS session this is could not be established, so the bridge will not end it")),+    }+}++#[cfg(test)]+mod tests {+    use super::*;++    fn sys(limit: u64, avail: u64) -> SystemMemory {+        SystemMemory { commit_limit_mb: limit, commit_avail_mb: avail, phys_total_mb: 32_000, phys_avail_mb: 4_000 }+    }++    fn pp(name: &str, pid: u32, mb: u64) -> ProcPrivate {+        ProcPrivate { name: name.into(), pid, private_mb: mb }+    }++    /// The numbers measured on 2026-10-14, before and after the quit.+    #[test]+    fn the_measured_incident_is_critical_and_the_quit_made_it_ok() {+        // 72.3 of 76.6 GB committed, 4.2 GB available, SOLIDWORKS 10.6 GB.+        let before = sys(78_438, 4_300);+        let (p, why) = pressure(Some(&before), Some(10_854), DEFAULT_BUDGET_MB);+        assert_eq!(p, Pressure::Critical, "{why:?}");+        assert!(why.iter().any(|w| w.contains("under 8 %")) && why.iter().any(|w| w.contains("over its 6.0 GB")), "{why:?}");+        // After the quit: 56.1 of 73.7 GB, 17.7 GB available, no SOLIDWORKS.+        assert_eq!(pressure(Some(&sys(75_469, 18_125)), None, DEFAULT_BUDGET_MB).0, Pressure::Ok);+    }++    #[test]+    fn thresholds() {+        let b = DEFAULT_BUDGET_MB;+        // Under 8 % is critical even with plenty of absolute room on a big PC...+        assert_eq!(pressure(Some(&sys(200_000, 15_000)), None, b).0, Pressure::Critical);+        // ...and under 4 GB is critical even when that is over 8 % of a small one.+        assert_eq!(pressure(Some(&sys(40_000, 4_000)), None, b).0, Pressure::Critical);+        assert_eq!(pressure(Some(&sys(40_000, 4_096)), None, b).0, Pressure::High, "10 % is high, not critical");+        // 8..15 % is high.+        assert_eq!(pressure(Some(&sys(100_000, 10_000)), None, b).0, Pressure::High);+        assert_eq!(pressure(Some(&sys(100_000, 15_000)), None, b).0, Pressure::Ok, "15 % exactly is ok");+        // SOLIDWORKS over its budget is high on an otherwise roomy machine.+        assert_eq!(pressure(Some(&sys(100_000, 50_000)), Some(b + 1), b).0, Pressure::High);+        assert_eq!(pressure(Some(&sys(100_000, 50_000)), Some(b), b).0, Pressure::Ok, "at the budget is not over it");+        // Unreadable commit is UNKNOWN, never ok — or high if SOLIDWORKS alone says so.+        assert_eq!(pressure(None, None, b).0, Pressure::Unknown);+        assert_eq!(pressure(Some(&sys(0, 0)), Some(100), b).0, Pressure::Unknown);+        assert_eq!(pressure(None, Some(b + 1), b).0, Pressure::High);+    }++    #[test]+    fn the_largest_holders_are_named_per_process_and_per_application() {+        let mut list = vec![pp("Fusion360.exe", 10, 21_197), pp("SLDWORKS.exe", 20, 10_854), pp("vmmemWSL", 30, 4_812)];+        for i in 0..51 {+            list.push(pp("chrome.exe", 100 + i, 200));+        }+        let top = top_processes(&list, TOP_N);+        assert_eq!(top.iter().map(|p| p.pid).collect::<Vec<_>>(), [10, 20, 30, 100, 101], "largest first, ties by pid");+        let images = top_images(&list, TOP_N);+        assert_eq!(images[0], ImageTotal { name: "Fusion360.exe".into(), processes: 1, private_mb: 21_197 });+        assert_eq!(images[2], ImageTotal { name: "chrome.exe".into(), processes: 51, private_mb: 10_200 });+        assert_eq!(+            holders_sentence(&images),+            "Fusion360.exe 20.7 GB, SLDWORKS.exe 10.6 GB, chrome.exe 10.0 GB (51 processes), vmmemWSL 4.7 GB"+        );+        // Case-insensitive grouping.+        assert_eq!(top_images(&[pp("Chrome.exe", 1, 10), pp("chrome.EXE", 2, 5)], 5)[0].processes, 2);+    }++    #[test]+    fn the_guard_refuses_only_at_critical_and_an_override_needs_a_reason() {+        assert_eq!(guard_decision(Pressure::Ok, Ok(None)), GuardDecision::Proceed);+        assert_eq!(guard_decision(Pressure::High, Ok(None)), GuardDecision::Proceed, "high warns, never refuses");+        assert_eq!(guard_decision(Pressure::Unknown, Ok(None)), GuardDecision::Proceed);+        assert_eq!(guard_decision(Pressure::Critical, Ok(None)), GuardDecision::Refuse { override_without_reason: false });+        assert_eq!(guard_decision(Pressure::Critical, Err(())), GuardDecision::Refuse { override_without_reason: true });+        assert_eq!(guard_decision(Pressure::Critical, Ok(Some("the user said go".into()))), GuardDecision::Overridden("the user said go".into()));+        assert_eq!(override_reason(&json!({})), Ok(None));+        assert_eq!(override_reason(&json!({"ignoreMemoryPressure": true})), Err(()));+        assert_eq!(override_reason(&json!({"ignoreMemoryPressure": true, "reason": "  short  "})), Err(()));+        assert_eq!(override_reason(&json!({"ignoreMemoryPressure": true, "reason": "Drew asked for it"})), Ok(Some("Drew asked for it".into())));+        assert_eq!(override_reason(&json!({"ignoreMemoryPressure": false, "reason": "Drew asked for it"})), Ok(None));+    }++    #[test]+    fn guarded_verbs_are_the_ones_that_create_or_load_geometry() {+        for v in ["swx_new_document", "swx_open", "swx_import", "swx_render_image", "swx_show_part", "swx_show_assembly", "swx_show_drawing"] {+            assert!(guarded(v, &json!({})), "{v}");+        }+        assert!(guarded("swx_selftest", &json!({"level": "write"})));+        assert!(!guarded("swx_selftest", &json!({"level": "read"})) && !guarded("swx_selftest", &json!({})));+        assert!(guarded("swx_run_script", &json!({"script": [{"op": "new_part"}, {"op": "rebuild"}]})));+        assert!(guarded("swx_run_script", &json!({"script": [{"op": "new_assembly"}]})));+        assert!(!guarded("swx_run_script", &json!({"script": [{"op": "rebuild"}]})), "a script on an open part loads nothing new");+        for v in ["swx_status", "swx_stop", "swx_close_doc", "swx_reset_workspace", "swx_inspect_bodies", "swx_export_step", "swx_docs"] {+            assert!(!guarded(v, &json!({})), "{v} frees or reads; never refused for memory");+        }+    }++    #[test]+    fn the_remedy_is_the_safe_one() {+        let ours = remedy(Some(true), Some(0), Some(10_854));+        assert!(ours.contains("swx_stop") && ours.contains("swx_start") && ours.contains("10.6 GB"), "{ours}");+        for (b, d) in [(Some(false), Some(0)), (None, None), (Some(false), Some(3)), (None, Some(0))] {+            let r = remedy(b, d, Some(10_854));+            assert!(r.contains("Tell the user") && r.contains("Never close them") && !r.contains("swx_stop"), "{b:?} {d:?}: {r}");+        }+        let busy = remedy(Some(true), Some(2), Some(10_854));+        assert!(busy.contains("2 document(s)") && busy.contains("never close"), "{busy}");+    }++    #[test]+    fn only_the_bridges_own_session_counts_as_the_bridges() {+        assert_eq!(bridge_started(Some((700, 1_000)), Some(700), Some(1_000)), Some(true));+        assert_eq!(bridge_started(Some((700, 1_000)), Some(700), Some(1_001)), Some(false), "pid reused: a different process");+        assert_eq!(bridge_started(Some((700, 1_000)), Some(701), Some(1_000)), Some(false));+        assert_eq!(bridge_started(None, Some(700), Some(1_000)), Some(false), "no launch record: not the bridge's");+        assert_eq!(bridge_started(Some((700, 1_000)), Some(700), None), None, "cannot tell is never yes");+        assert_eq!(bridge_started(Some((700, 1_000)), None, None), None);+    }++    #[test]+    fn a_long_run_recycles_only_its_own_empty_session_and_otherwise_stops() {+        let b = DEFAULT_BUDGET_MB;+        assert_eq!(between_checks(Pressure::Ok, Some(1_000), b, Some(false), Some(4)), Between::Continue);+        assert!(matches!(between_checks(Pressure::Ok, Some(b + 1), b, Some(true), Some(0)), Between::Recycle(_)));+        assert!(matches!(between_checks(Pressure::High, Some(3_000), b, Some(true), Some(0)), Between::Recycle(_)));+        assert!(matches!(between_checks(Pressure::Critical, Some(RECYCLE_WORTH_MB), b, Some(true), Some(0)), Between::Recycle(_)));+        // Pressure that SOLIDWORKS is not causing: a restart would not relieve it.+        for sw in [Some(1_000), None] {+            match between_checks(Pressure::Critical, sw, b, Some(true), Some(0)) {+                Between::Stop(why) => assert!(why.contains("held by other applications"), "{why}"),+                other => panic!("{other:?}"),+            }+        }+        for (bs, d) in [(Some(true), Some(1)), (Some(true), None), (Some(false), Some(0)), (None, Some(0)), (None, None)] {+            match between_checks(Pressure::High, Some(b + 1), b, bs, d) {+                Between::Stop(why) => assert!(why.contains("SOLIDWORKS holds") && why.contains("memory pressure is high"), "{why}"),+                other => panic!("{bs:?} {d:?}: {other:?} — only the bridge's own empty session is recycled"),+            }+        }+        assert!(matches!(between_checks(Pressure::Unknown, Some(10), b, None, None), Between::Continue), "nothing to act on");+    }++    #[test]+    fn the_report_and_its_hint_name_the_holders() {+        let s = Sample {+            sys: Ok(sys(78_438, 4_300)),+            sw_pid: Some(20),+            sw_private_mb: Some(10_854),+            sw_handles: Some(4_000),+            budget_mb: DEFAULT_BUDGET_MB,+            budget_source: "default",+        };+        let r = report_of(&s, Ok(vec![pp("Fusion360.exe", 10, 21_197), pp("SLDWORKS.exe", 20, 10_854)]));+        assert_eq!(r["pressure"], "critical");+        assert_eq!(r["commitUsedMb"], 78_438 - 4_300);+        assert_eq!(r["commitUsedPercent"], 94.5);+        assert_eq!(r["solidworks"]["overBudget"], true);+        assert_eq!(r["topProcesses"][0]["name"], "Fusion360.exe");+        let h = pressure_hint(&r).unwrap();+        assert!(h.starts_with("MEMORY CRITICAL: 72.4 GB of 76.6 GB committed (94.5 %), 4.2 GB available."), "{h}");+        assert!(h.contains("Fusion360.exe 20.7 GB, SLDWORKS.exe 10.6 GB") && h.contains("NEVER close the user's applications"), "{h}");+        // Nothing to say when it is fine.+        let ok = report_of(&Sample { sys: Ok(sys(78_438, 40_000)), sw_private_mb: Some(1_000), ..s.clone() }, Ok(vec![]));+        assert_eq!(pressure_hint(&ok), None);+        // An unreadable process list is said, not hidden.+        let r = report_of(&s, Err("no".into()));+        assert!(r["topProcesses"].is_null() && r["topProcessesError"] == "no");+    }+}
src/selftest.rs+209−4
@@ -169,15 +169,50 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {             .and_then(|o| o.as_array())             .map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())             .unwrap_or_default();+        // MEMORY, BETWEEN EVERY CHECK (0.10.5). Measured 2026-10-14: about a+        // hundred scratch parts built and closed in one bridge-started session+        // left SOLIDWORKS holding 10.6 GB with no documents open, and the+        // machine at 94 % committed. Before each check the run reads memory;+        // over the session budget or under pressure it RECYCLES a session the+        // bridge started with nothing open (swx_stop, swx_start, ready), and+        // otherwise STOPS — it never ends a session that is not its own.+        let memory_override = crate::memory::override_reason(args).ok().flatten();+        let mut memory_log = MemoryLog::default();+        let mut last_check: Option<&'static str> = None;         for check in battery() {             if !only.is_empty() && !only.iter().any(|o| o == check.id) {                 continue;             }-            geometry_check(ctx, &mut run, &check);+            if let Err(stop) = memory_between(ctx, last_check, check.id, memory_override.as_deref(), &mut memory_log) {+                memory_log.stopped = Some(stop);+                break;+            }+            geometry_check(ctx, &mut run, &check, memory_override.as_deref());+            let after = crate::memory::sample(crate::launchos::os().as_ref());+            if let Some(last) = run.results.last_mut() {+                last["swPrivateMbAfter"] = json!(after.sw_private_mb);+            }+            last_check = Some(check.id);+        }+        if memory_log.stopped.is_none() {+            if let Err(stop) = memory_between(ctx, last_check, "the verbs on a scratch part", memory_override.as_deref(), &mut memory_log) {+                memory_log.stopped = Some(stop);+            }+        }+        if memory_log.stopped.is_some() {+            if let Some(prev) = &active {+                run.call(ctx, "re-activate the document that was active before", "swx_activate_document", json!({"name": prev}));+            }+            return finish_with(level, &sw, ready, true, started, run, None, memory_log.to_json(memory_override.as_deref()));         }          // THE VERBS on one scratch part made by swx_new_document.-        let made = run.call(ctx, "new scratch part", "swx_new_document", json!({"type": "part"}));+        let mut new_doc = json!({"type": "part"});+        if let Some(r) = &memory_override {+            new_doc["ignoreMemoryPressure"] = json!(true);+            new_doc["reason"] = json!(r);+        }+        let made = run.call(ctx, "new scratch part", "swx_new_document", new_doc);         let title = made.pointer("/created/title").and_then(|t| t.as_str()).map(String::from);         let path = made.pointer("/documentIdentity/path").and_then(|t| t.as_str()).map(String::from);         if let Some(title) = title {@@ -224,14 +259,179 @@ pub fn selftest(ctx: &Ctx, args: &Value) -> Value {         if let Some(prev) = &active {             run.call(ctx, "re-activate the document that was active before", "swx_activate_document", json!({"name": prev}));         }+        return finish_with(level, &sw, ready, true, started, run, None, memory_log.to_json(memory_override.as_deref()));     }      finish(level, &sw, ready, true, started, run, None) } +/// What the memory checks between the geometry checks did.+#[derive(Default)]+struct MemoryLog {+    recycled: Vec<Value>,+    /// Checks run on under `ignoreMemoryPressure` where the run would have stopped.+    overridden: Vec<Value>,+    stopped: Option<Value>,+}++impl MemoryLog {+    fn to_json(&self, over: Option<&str>) -> Value {+        let mut v = json!({+            "recycled": self.recycled,+            "complete": self.stopped.is_none(),+            "memory": crate::memory::report(crate::launchos::os().as_ref()),+        });+        if let Some(stop) = &self.stopped {+            v["stoppedFor"] = json!("memory");+            v["stopped"] = stop.clone();+        }+        if let Some(r) = over {+            v["memoryOverride"] = json!({"reason": r, "continuedWhereItWouldHaveStopped": self.overridden});+        }+        v+    }+}++/// Before one geometry check: read memory, and recycle or stop if it calls+/// for it. `Err` stops the run, with everything that explains why.+fn memory_between(ctx: &Ctx, last: Option<&str>, next: &str, over: Option<&str>, log: &mut MemoryLog) -> Result<(), Value> {+    use crate::memory::Between;+    let os = crate::launchos::os();+    let s = crate::memory::sample(os.as_ref());+    let (p, _) = s.pressure();+    if !crate::memory::needs_attention(p, s.sw_private_mb, s.budget_mb) {+        return Ok(());+    }+    let facts = crate::memory::session_facts(ctx, os.as_ref());+    match crate::memory::between_checks(p, s.sw_private_mb, s.budget_mb, facts.bridge_started, facts.open_docs) {+        Between::Continue => Ok(()),+        Between::Recycle(why) => {+            let r = recycle(ctx, last, next, &s, &why)?;+            log.recycled.push(r);+            Ok(())+        }+        Between::Stop(why) => {+            if over.is_some() {+                log.overridden.push(json!({"beforeCheck": next, "why": why}));+                return Ok(());+            }+            Err(json!({+                "beforeCheck": next,+                "afterCheck": last,+                "why": why,+                "solidworksSession": {"pid": facts.com_pid, "bridgeStarted": facts.bridge_started, "openDocuments": facts.open_docs},+                "remedy": crate::memory::remedy(facts.bridge_started, facts.open_docs, s.sw_private_mb),+            }))+        }+    }+}++fn compact(v: &Value) -> Value {+    json!({+        "success": v.get("success"),+        "errorCode": v.get("errorCode"),+        "error": v.get("error"),+        "stopped": v.get("stopped"),+        "winningRung": v.get("winningRung"),+        "attached": v.get("attached"),+        "stillLaunching": v.get("stillLaunching"),+    })+}++fn env_u64(name: &str, default: u64) -> u64 {+    std::env::var(name).ok().and_then(|v| v.trim().parse().ok()).unwrap_or(default)+}++/// RECYCLE THE BRIDGE'S OWN, EMPTY SESSION: swx_stop (every one of its own+/// refusals intact — documents, dialogs, the user's presence, identity), then+/// swx_start, then wait until SOLIDWORKS is ready. Anything short of that STOPS+/// the run.+fn recycle(ctx: &Ctx, last: Option<&str>, next: &str, before: &crate::memory::Sample, why: &str) -> Result<Value, Value> {+    let t = Instant::now();+    let reason = format!("swx_selftest is recycling the bridge's own SOLIDWORKS session before {next}: {why}");+    let fail = |at: &str, detail: Value| {+        json!({"beforeCheck": next, "afterCheck": last, "why": why, "recycleFailedAt": at, "detail": detail})+    };+    let stop = crate::verbs::dispatch(ctx, "swx_stop", &json!({"userAuthorized": true, "reason": reason}));+    if stop.get("stopped").and_then(|v| v.as_bool()) != Some(true) {+        return Err(fail("swx_stop", compact(&stop)));+    }+    let start = crate::verbs::dispatch(ctx, "swx_start", &json!({"reason": reason, "waitSeconds": 240}));+    let budget = std::time::Duration::from_secs(env_u64("SWX_RECYCLE_READY_SECS", 360));+    let poll = std::time::Duration::from_millis(env_u64("SWX_RECYCLE_POLL_MS", 3000));+    loop {+        let r = crate::readiness::assess(ctx);+        if r.get("ready").and_then(|v| v.as_bool()) == Some(true) {+            break;+        }+        if t.elapsed() >= budget {+            return Err(fail("ready", json!({"swxStart": compact(&start), "readiness": r})));+        }+        std::thread::sleep(poll);+    }+    let after = crate::memory::sample(crate::launchos::os().as_ref());+    let freed = match (before.commit_avail_mb(), after.commit_avail_mb()) {+        (Some(b), Some(a)) => Some(a as i64 - b as i64),+        _ => match (before.sw_private_mb, after.sw_private_mb) {+            (Some(b), Some(a)) => Some(b as i64 - a as i64),+            (Some(b), None) => Some(b as i64),+            _ => None,+        },+    };+    let row = json!({+        "afterCheck": last,+        "beforeCheck": next,+        "freedMb": freed,+        "why": why,+        "solidworksPrivateMbBefore": before.sw_private_mb,+        "solidworksPrivateMbAfter": after.sw_private_mb,+        "commitAvailableMbBefore": before.commit_avail_mb(),+        "commitAvailableMbAfter": after.commit_avail_mb(),+        "seconds": t.elapsed().as_secs(),+    });+    crate::ledger::record("solidworks_recycled", row.clone());+    Ok(row)+}+ /// The reply: the rows, the counts, and — when SOLIDWORKS was not there — /// that FIRST, so nobody reads a skipped check as a failure. fn finish(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>) -> Value {+    finish_with(level, sw, ready, running, started, run, lead, json!({}))+}++/// `finish`, with the write level's memory record merged in (0.10.5).+#[allow(clippy::too_many_arguments)]+fn finish_with(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>, extra: Value) -> Value {+    let recycled = extra.get("recycled").and_then(|r| r.as_array()).map(|a| a.len()).unwrap_or(0);+    let lead = match recycled {+        0 => lead,+        n => Some(format!(+            "MEMORY: this run recycled the bridge's own SOLIDWORKS session {} (see `recycled`: when, \+             why, and what it freed). {}",+            if n == 1 { "once".to_string() } else { format!("{n} times") },+            lead.unwrap_or_default()+        )),+    };+    let lead = match extra.get("stopped") {+        Some(stop) => Some(format!(+            "THE RUN STOPPED FOR MEMORY before {}: {}. Every row above that point is a real result; \+             nothing after it ran. {}",+            stop["beforeCheck"].as_str().unwrap_or("the next check"),+            stop["why"].as_str().unwrap_or("memory"),+            lead.unwrap_or_default()+        )),+        None => lead,+    };+    let mut out = finish_base(level, sw, ready, running, started, run, lead);+    if let (Value::Object(m), Value::Object(e)) = (&mut out, extra) {+        for (k, v) in e {+            m.insert(k, v);+        }+    }+    out+}++fn finish_base(level: &str, sw: &Value, ready: bool, running: bool, started: Instant, run: Run, lead: Option<String>) -> Value {     let passed = run.results.iter().filter(|r| r["pass"] == true).count();     let failed = run.results.iter().filter(|r| r["pass"] == false).count();     let skipped = run.results.iter().filter(|r| r["pass"].is_null()).count();@@ -503,10 +703,15 @@ fn titles(ctx: &Ctx) -> Vec<String> { }  /// One check: build, MEASURE, compare, close what it made.-fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check) {+fn geometry_check(ctx: &Ctx, run: &mut Run, c: &Check, memory_override: Option<&str>) {     let t = Instant::now();     let before = titles(ctx);-    let r = crate::verbs::dispatch(ctx, "swx_run_script", &json!({"script": c.script}));+    let mut sargs = json!({"script": c.script});+    if let Some(r) = memory_override {+        sargs["ignoreMemoryPressure"] = json!(true);+        sargs["reason"] = json!(r);+    }+    let r = crate::verbs::dispatch(ctx, "swx_run_script", &sargs);     let ok = r.get("success").and_then(|v| v.as_bool()) != Some(false);     let doc = r.get("documentName").and_then(|d| d.as_str()).map(String::from);     let mut fails: Vec<String> = Vec::new();
src/server.rs+3−1
@@ -300,7 +300,9 @@ fn handle(ctx: &Arc<Ctx>, mut req: Request) {                 args_str.truncate(cut);                 args_str.push('…');             }-            println!("[swx] {command}{} | args: {args_str}", crate::caller::log_suffix());+            // ...with the memory sample (0.10.5): commit available and+            // SOLIDWORKS's own share, so a long run's growth reads off the log.+            println!("[swx] {command}{} | args: {args_str}{}", crate::caller::log_suffix(), crate::memory::log_suffix());              // A verb panic becomes a JSON error the caller can read (and report),             // not a dropped connection or a dead worker.
src/verbs.rs+27
@@ -635,6 +635,10 @@ fn swx_status(ctx: &Ctx, _args: &Value) -> Value {         "abAvailable": crate::caller::ab_available(),         "solidworks": solidworks,         "com": com,+        // THE MACHINE'S MEMORY (0.10.5): the commit charge, SOLIDWORKS's own+        // share, and the largest holders — read-only facts. Read it before a+        // long run (the solidworks-memory skill).+        "memory": crate::memory::report(os.as_ref()),         "windowEtiquette": "background",         "_hint": hint,     }))@@ -3332,6 +3336,24 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value {             }         }     }+    // THE MEMORY GUARD (0.10.5): a verb that creates or loads geometry is+    // refused while committed memory is CRITICAL — measured 2026-10-14, the+    // SOLIDWORKS Resource Monitor warned at 94 % committed — and the refusal+    // changes nothing. `ignoreMemoryPressure:true` with a reason overrides, and+    // the override is ledgered. See src/memory.rs.+    let mut memory_override: Option<Value> = None;+    if crate::memory::guarded(command, args) {+        match crate::memory::guard(ctx, command, args) {+            Ok(o) => memory_override = o,+            Err(refusal) => {+                let mut refusal = refusal;+                if let (Some(summary), Value::Object(m)) = (at_gate.clone(), &mut refusal) {+                    m.entry("readiness").or_insert(summary);+                }+                return refusal;+            }+        }+    }     let changes_documents = is_known(command)         && startup_touch(command) == crate::readiness::Touch::Mutate         && !is_startup_exempt(command);@@ -3345,6 +3367,9 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value {     if let (Some(evidence), Value::Object(m)) = (presence, &mut out) {         m.entry("userPresence").or_insert(evidence);     }+    if let (Some(o), Value::Object(m)) = (memory_override, &mut out) {+        m.entry("memoryOverride").or_insert(o);+    }     if changes_documents {         // A verb that adds documents on a session the user has taken over is         // allowed — it adds, never removes — and says so.@@ -3360,6 +3385,8 @@ pub fn dispatch(ctx: &Ctx, command: &str, args: &Value) -> Value {         // user's.         crate::presence::note_bridge_changed(ctx, before);     }+    // EVERY REPLY, while memory is pressing, names the largest holders (0.10.5).+    crate::memory::annotate(&mut out);     out } 
src/verbs3.rs+2−2
@@ -4732,9 +4732,9 @@ fn script_help(message: impl Into<String>) -> Value {             "conventions".into(),             json!([                 "Every length is MILLIMETRES; angles are degrees.",-                "A sketch's x/y are its PLANE's own axes. Top Plane: sketch +x = model +X and sketch +y = model −Z (measured on SOLIDWORKS 2021, issue #1). Front Plane: sketch (x, y) = model (X, Y). Right Plane: sketch +x = model −Z, +y = model +Y (unmeasured).",+                "A sketch's x/y are its PLANE's own axes. Top Plane: sketch +x = model +X and sketch +y = model −Z (measured on SOLIDWORKS 2021, issue #1). Front Plane: sketch (x, y) = model (X, Y). Right Plane: sketch +x = model −Z, +y = model +Y, and a boss goes +X (measured on SOLIDWORKS 2026, T35).",                 "sketch_rect xMm/yMm is the CORNER unless anchor:\"center\" (or centerMm is given).",-                "An offset plane sits offsetMm along its base plane's normal (Top +Y, Front +Z, Right +X; negative = the other side) and keeps the base plane's sketch axes. A face sketch (only faces parallel to Top/Front/Right) uses the parallel default plane's sketch axes too; the face is picked by GEOMETRY (the face through your point), never through the view — a hidden face works.",+                "An offset plane sits offsetMm along its base plane's normal (Top +Y, Front +Z, Right +X; negative = the other side) and keeps the base plane's sketch axes. A face sketch (only faces parallel to Top/Front/Right) uses the parallel default plane's sketch axes too, with the origin projected onto the face (measured on 2026: top, bottom, +X and −Z faces); the face is picked by GEOMETRY (the face through your point), never through the view — a hidden face works.",                 "An extrude goes along the plane's +normal; a CUT goes along its −NORMAL (measured: a cut from Top Plane removes −Y). From a FACE: an extrude goes OUTWARD (away from the material), a cut INWARD — on any face, the bottom one included. reverse:true flips either.",                 "direction:\"both\": depthMm goes the op's OWN way (an extrude's +normal / outward, a cut's −normal / inward), depth2Mm the other way. midPlane:true is symmetric, depthMm in total.",                 "A material wipes a part's appearance (SOLIDWORKS behaviour): the bridge RE-APPLIES every colour the script set before a set_material (the step says so) — putting set_material first needs nothing re-applied.",
tests/issue1.rs+15−5
@@ -374,7 +374,9 @@ fn steps_say_what_was_measured_live_on_this_release() {         {"op": "sketch_circle", "face": {"atPointMm": [0, 10, 0]}, "radiusMm": 3},         {"op": "extrude", "depthMm": 2},         {"op": "shell", "thicknessMm": 1, "removeFacesAtPointsMm": [[5, 0, 5]]},-        {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 4, "heightMm": 4, "anchor": "center", "yMm": 5},+        {"op": "sketch_rect", "plane": {"offsetFrom": "Front Plane", "offsetMm": -15}, "widthMm": 4, "heightMm": 4, "anchor": "center", "yMm": 5},+        {"op": "extrude", "depthMm": 3},+        {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 4, "heightMm": 4, "anchor": "center", "xMm": 30},         {"op": "extrude", "depthMm": 3, "reverse": true}     ]);     // The simulated install is SOLIDWORKS 2026, the release that was measured.@@ -397,10 +399,18 @@ fn steps_say_what_was_measured_live_on_this_release() {     );     check!(         b,-        "a REVERSED boss was never built live: live:false, and the step says which variant is unmeasured",-        st[7]["result"]["proven"]["live"] == false-            && st[7]["result"]["proven"]["unmeasured"][0]["variant"] == "extrude.one.reversed",-        s(&st[7])+        "a NEGATIVE offset from Front Plane was never built live: live:false, and the step names the variant",+        st[6]["result"]["proven"]["live"] == false+            && st[6]["result"]["proven"]["unmeasured"][0]["variant"] == "sketch.offset.front.negative",+        s(&st[6])+    );+    check!(+        b,+        "a REVERSED boss was measured by 0.10.4's checks (T33): live, with THAT measurement",+        st[9]["result"]["proven"]["live"] == true+            && st[9]["result"]["proven"]["measuredOn"]+                == json!([{"solidworks": 2026, "major": 34, "edition": "connected", "date": "2026-10-14", "bridge": "0.10.4"}]),+        s(&st[9])     );     // The same script on SOLIDWORKS 2021: nothing is live there yet.     std::fs::write(b.scratch.join("mock-revision"), "29.5.1").unwrap();
tests/memory.rsadded+343
@@ -0,0 +1,343 @@+//! MEMORY (0.10.5) — the incident of 2026-10-14, and what the bridge now does+//! about it.+//!+//! MEASURED on adom_drew2: the SOLIDWORKS Resource Monitor warned "Available+//! committed memory is critically low" at 72.3 of 76.6 GB committed, 4.2 GB+//! available. Fusion 360 held 20.7 GB (the user's), the bridge-started+//! SOLIDWORKS 10.6 GB with zero documents open, Chrome 10.2 GB over 51+//! processes. Quitting that SOLIDWORKS returned about 16 GB.+//!+//! Everything here runs in the SIMULATOR (`memory.json` in the simulated+//! machine): the numbers are the measured ones, the behaviour is the bridge's.++mod common;++use common::{Bridge, TempDir};+use serde_json::{json, Value};++fn s(v: &Value) -> String {+    serde_json::to_string_pretty(v).unwrap_or_default()+}++/// The machine on 2026-10-14 (MB): the commit pair, and the holders other than+/// SOLIDWORKS (which the simulator adds from `sldworks.json`).+fn incident(avail_mb: u64) -> Value {+    let mut procs = vec![+        json!({"name": "Fusion360.exe", "pid": 41000, "privateMB": 21197}),+        json!({"name": "vmmemWSL", "pid": 41500, "privateMB": 4812}),+    ];+    for i in 0..3 {+        procs.push(json!({"name": "chrome.exe", "pid": 42000 + i, "privateMB": 3400}));+    }+    json!({"commitLimitMB": 78438, "commitAvailMB": avail_mb, "physTotalMB": 32153, "physAvailMB": 3000, "processes": procs})+}++struct Machine {+    dir: TempDir,+}++impl Machine {+    fn new(tag: &str, memory: Value, sw_private_mb: u64) -> Machine {+        let dir = TempDir::new(tag);+        std::fs::write(dir.join("windows.json"), "[]").unwrap();+        std::fs::write(dir.join("memory.json"), memory.to_string()).unwrap();+        std::fs::write(+            dir.join("sldworks.json"),+            json!({"responding": true, "hwnd": 6622700, "privateMB": sw_private_mb, "handles": 4000}).to_string(),+        )+        .unwrap();+        Machine { dir }+    }++    fn env(&self) -> Vec<(&'static str, &'static str)> {+        vec![+            ("SWX_LAUNCH_FAKE_DIR", Box::leak(self.dir.path().into_boxed_str())),+            ("SWX_QUIT_SETTLE_MS", "80"),+            ("SWX_QUIT_EXIT_WAIT_SECS", "3"),+            ("SWX_RECYCLE_POLL_MS", "100"),+            ("SWX_RECYCLE_READY_SECS", "30"),+        ]+    }++    fn boot(&self) -> Bridge {+        Bridge::start_with(&self.env())+    }++    /// A SOLIDWORKS session THIS BRIDGE LAUNCHED: the launch record's claim+    /// names the very process the simulator runs (pid AND creation time).+    fn boot_as_bridge_started(&self, quit_script: Value) -> Bridge {+        std::fs::write(self.dir.join("quit-script.json"), quit_script.to_string()).unwrap();+        std::fs::write(self.dir.join("sldworks-created"), "1700000000").unwrap();+        let scratch = TempDir::new("memory-scratch");+        std::fs::write(scratch.join("mock-solidworks-session"), "700900").unwrap();+        std::fs::write(+            scratch.join("launch-state.json"),+            json!({+                "attempt": {+                    "id": 1, "startedAt": 1700000000u64, "shortcut": "x.lnk",+                    "route": "shellExecute", "invokedAt": 1700000000u64,+                    "stage": "attached", "finished": true, "outcome": "attached",+                    "runBeginSeen": true,+                    "attributedSolidworks": {"pid": 700900, "createdAt": 1700000000u64},+                },+                "preferredRoute": "shellExecute", "consecutiveFailures": 0,+            })+            .to_string(),+        )+        .unwrap();+        Bridge::start_in(scratch, &self.env())+    }++    fn quit_happened(&self) -> bool {+        self.dir.join("quit.jsonl").exists()+    }++    /// NOTHING WAS EVER KILLED: the simulator records every terminate.+    fn nothing_terminated(&self) -> bool {+        !self.dir.join("terminated.jsonl").exists()+    }+}++fn doc_count(b: &Bridge) -> usize {+    b.call("swx_docs", json!({}))["docs"].as_array().map(|a| a.len()).unwrap_or(usize::MAX)+}++fn ledger(b: &Bridge) -> Vec<Value> {+    b.call("swx_action_ledger", json!({"limit": 500}))["actions"].as_array().cloned().unwrap_or_default()+}++#[test]+fn status_reports_the_commit_charge_and_the_largest_holders() {+    let m = Machine::new("mem-status", incident(4300), 10854);+    let b = m.boot();+    b.call("swx_start", json!({}));+    let r = b.call("swx_status", json!({}));+    let mem = &r["memory"];+    check!(+        b,+        "swx_status.memory: the commit pair, as GlobalMemoryStatusEx gives it (simulated)",+        mem["commitLimitMb"] == 78438 && mem["commitAvailableMb"] == 4300 && mem["commitUsedMb"] == 78438 - 4300+            && mem["commitUsedPercent"] == 94.5,+        s(mem)+    );+    check!(+        b,+        "SOLIDWORKS's own share, the 6 GB default budget, and CRITICAL pressure",+        mem["solidworks"]["privateMb"] == 10854 && mem["solidworks"]["overBudget"] == true+            && mem["sessionBudgetMb"] == 6144 && mem["pressure"] == "critical",+        s(mem)+    );+    check!(+        b,+        "the top holders, per process and per application, Fusion first",+        mem["topProcesses"][0]["name"] == "Fusion360.exe" && mem["topProcesses"].as_array().map(|a| a.len()) == Some(5)+            && mem["topImages"].as_array().unwrap().iter().any(|i| i["name"] == "chrome.exe" && i["processes"] == 3),+        s(mem)+    );+    check!(+        b,+        "every reply's _hint names the largest holders while pressure is not ok",+        r["_hint"].as_str().unwrap_or("").contains("MEMORY CRITICAL")+            && b.call("swx_docs", json!({}))["_hint"].as_str().unwrap_or("").contains("Fusion360.exe 20.7 GB"),+        s(&r["_hint"])+    );+    check!(b, "the one log line per verb carries the memory sample", b.log().contains("| mem: commit available 4.2 GB"), "log");+    check!(b, "read-only: nothing was terminated", m.nothing_terminated(), "terminated.jsonl exists");+}++#[test]+fn critical_memory_refuses_geometry_and_changes_nothing() {+    let m = Machine::new("mem-guard", incident(4300), 2000);+    let b = m.boot();+    b.call("swx_start", json!({}));+    let before = doc_count(&b);+    let r = b.call("swx_new_document", json!({"type": "part"}));+    check!(+        b,+        "swx_new_document is refused low_commit_memory, with the numbers and the holders",+        r["success"] == false && r["errorCode"] == "low_commit_memory" && r["changedNothing"] == true+            && r["error"].as_str().unwrap_or("").contains("4.2 GB available")+            && r["largestHolders"].as_str().unwrap_or("").starts_with("Fusion360.exe 20.7 GB"),+        s(&r)+    );+    check!(+        b,+        "not the bridge's session: the remedy is to tell the user, never to close anything",+        r["remedy"].as_str().unwrap_or("").contains("Tell the user") && r["remedy"].as_str().unwrap_or("").contains("Never close them")+            && !r["remedy"].as_str().unwrap_or("").contains("swx_stop"),+        s(&r)+    );+    check!(b, "...and nothing was created", doc_count(&b) == before, format!("{before} -> {}", doc_count(&b)));+    let script = json!([{"op": "new_part"}, {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 10, "heightMm": 10}, {"op": "extrude", "depthMm": 5}]);+    let r = b.call("swx_run_script", json!({"script": script}));+    check!(b, "a script that makes a new part is refused too, and creates nothing", r["errorCode"] == "low_commit_memory" && doc_count(&b) == before, s(&r));+    for (verb, args) in [+        ("swx_open", json!({"path": "C:\\designs\\x.sldprt"})),+        ("swx_import", json!({"path": "C:\\designs\\x.step"})),+        ("swx_render_image", json!({"width": 100, "height": 100})),+        ("swx_show_part", json!({})),+        ("swx_selftest", json!({"level": "write"})),+    ] {+        let r = b.call(verb, args);+        check!(b, &format!("{verb} is refused at critical"), r["errorCode"] == "low_commit_memory", s(&r));+    }+    let r = b.call("swx_selftest", json!({"level": "read"}));+    check!(b, "a read-only self-test is not refused", r["errorCode"] != "low_commit_memory" && r["success"] == true, s(&r["summary"]));+    let r = b.call("swx_new_document", json!({"type": "part", "ignoreMemoryPressure": true}));+    check!(+        b,+        "an override without a reason is still refused",+        r["errorCode"] == "low_commit_memory" && r["error"].as_str().unwrap_or("").contains("needs a reason"),+        s(&r)+    );+    let r = b.call("swx_new_document", json!({"type": "part", "ignoreMemoryPressure": true, "reason": "Drew said build it anyway"}));+    check!(+        b,+        "ignoreMemoryPressure with a reason runs it, and the reply says so",+        r["success"] == true && r["memoryOverride"]["overridden"] == true && doc_count(&b) == before + 1,+        s(&r)+    );+    let l = ledger(&b);+    check!(+        b,+        "the override and the refusals are in the ledger, each line with its memory sample",+        l.iter().any(|e| e["action"] == "memory_guard_overridden" && e["detail"]["reason"] == "Drew said build it anyway")+            && l.iter().any(|e| e["action"] == "memory_guard_refused" && e["mem"]["commitAvailMb"] == 4300),+        s(&json!(l))+    );+    check!(b, "nothing was quit or terminated", !m.quit_happened() && m.nothing_terminated(), "quit.jsonl / terminated.jsonl");+}++#[test]+fn high_memory_proceeds_and_warns() {+    // 12 % available: high, not critical.+    let m = Machine::new("mem-high", incident(9400), 2000);+    let b = m.boot();+    b.call("swx_start", json!({}));+    let r = b.call("swx_new_document", json!({"type": "part"}));+    check!(+        b,+        "high pressure: the document is made, and the _hint warns with the holders",+        r["success"] == true && r["_hint"].as_str().unwrap_or("").contains("MEMORY HIGH")+            && r["memoryPressure"]["pressure"] == "high",+        s(&r)+    );+}++#[test]+fn the_remedy_for_the_bridges_own_empty_session_is_stop_then_start() {+    let m = Machine::new("mem-ours", incident(4300), 10854);+    let b = m.boot_as_bridge_started(json!({"a": "exited"}));+    b.call("swx_start", json!({}));+    let r = b.call("swx_new_document", json!({"type": "part"}));+    check!(+        b,+        "a session the bridge started, with no documents open: swx_stop then swx_start frees about 10.6 GB",+        r["errorCode"] == "low_commit_memory" && r["solidworksSession"]["bridgeStarted"] == true+            && r["solidworksSession"]["openDocuments"] == 0+            && r["remedy"].as_str().unwrap_or("").contains("swx_stop")+            && r["remedy"].as_str().unwrap_or("").contains("frees about 10.6 GB"),+        s(&r)+    );+    check!(b, "the guard itself never quits anything", !m.quit_happened() && m.nothing_terminated(), "quit.jsonl / terminated.jsonl");+}++#[test]+fn a_long_selftest_stops_rather_than_end_a_session_that_is_not_its_own() {+    // Commit is fine; SOLIDWORKS is over its 6 GB budget; the session is NOT+    // the bridge's.+    let m = Machine::new("mem-stop", incident(40000), 9000);+    let b = m.boot();+    b.call("swx_start", json!({}));+    let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03"]}));+    let rows = r["results"].as_array().cloned().unwrap_or_default();+    check!(+        b,+        "the run STOPS for memory before its first check, with a full report of what ran",+        r["success"] == true && r["stoppedFor"] == "memory" && r["complete"] == false+            && r["stopped"]["beforeCheck"] == "T01"+            && r["stopped"]["why"].as_str().unwrap_or("").contains("not started by the bridge")+            && !rows.iter().any(|x| x["name"].as_str().unwrap_or("").starts_with("T0")),+        s(&r)+    );+    check!(b, "the reply's hint leads with the stop", r["_hint"].as_str().unwrap_or("").starts_with("THE RUN STOPPED FOR MEMORY"), s(&r["_hint"]));+    check!(+        b,+        "it never tried to end the user's session: no quit, no swx_stop attempt, nothing terminated",+        !m.quit_happened() && m.nothing_terminated()+            && !ledger(&b).iter().any(|e| e["action"] == "solidworks_quit_refused" || e["action"] == "solidworks_recycled"),+        s(&json!(ledger(&b)))+    );+}++#[test]+fn a_long_selftest_with_a_document_open_stops_even_in_its_own_session() {+    let m = Machine::new("mem-busy", incident(40000), 9000);+    let b = m.boot_as_bridge_started(json!({"a": "exited"}));+    b.call("swx_start", json!({}));+    b.call("swx_new_document", json!({"type": "part"}));+    let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01"]}));+    check!(+        b,+        "a document is open: no recycle, the run stops and says why",+        r["stoppedFor"] == "memory" && r["stopped"]["why"].as_str().unwrap_or("").contains("1 document(s) are open")+            && r["recycled"] == json!([]),+        s(&r)+    );+    check!(b, "...and nothing was quit or terminated", !m.quit_happened() && m.nothing_terminated(), "quit.jsonl / terminated.jsonl");+}++#[test]+fn a_long_selftest_recycles_its_own_empty_session_and_carries_on() {+    // SOLIDWORKS over budget in a session the bridge started, nothing open.+    // After the quit the simulator's commit recovers (afterQuit).+    let mut mem = incident(40000);+    mem["afterQuit"] = json!({"commitAvailMB": 52000});+    let m = Machine::new("mem-recycle", mem, 9000);+    let b = m.boot_as_bridge_started(json!({"a": "exited"}));+    b.call("swx_start", json!({}));+    let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01", "T03"]}));+    let rec = r["recycled"].as_array().cloned().unwrap_or_default();+    check!(+        b,+        "ONE recycle, before the first check, with what it freed",+        rec.len() == 1 && rec[0]["beforeCheck"] == "T01" && rec[0]["afterCheck"].is_null()+            && rec[0]["freedMb"] == 12000 && rec[0]["solidworksPrivateMbBefore"] == 9000,+        s(&r)+    );+    let rows = r["results"].as_array().cloned().unwrap_or_default();+    let t01 = rows.iter().find(|x| x["name"].as_str().unwrap_or("").starts_with("T01")).cloned().unwrap_or(Value::Null);+    check!(+        b,+        "the run carried on and completed, each check row carrying swPrivateMbAfter",+        r["complete"] == true && r.get("stoppedFor").is_none() && t01["pass"] == true+            && r["_hint"].as_str().unwrap_or("").contains("recycled the bridge's own SOLIDWORKS session once")+            && t01.as_object().map(|o| o.contains_key("swPrivateMbAfter")).unwrap_or(false),+        s(&r)+    );+    check!(+        b,+        "the recycle went through swx_stop (a quit happened, ledgered) and never killed anything",+        m.quit_happened() && m.nothing_terminated() && ledger(&b).iter().any(|e| e["action"] == "solidworks_recycled"),+        s(&json!(ledger(&b)))+    );+}++#[test]+fn a_recycle_that_cannot_quit_stops_the_run_and_says_where() {+    // The bridge's own empty session, but SOLIDWORKS declines ExitApp (rung a),+    // and the ladder is pinned at rung a: the quit does not happen.+    let m = Machine::new("mem-declined", incident(40000), 9000);+    let b = m.boot_as_bridge_started(json!({"a": "declined"}));+    b.call("swx_start", json!({}));+    let r = b.call("swx_selftest", json!({"level": "write", "only": ["T01"]}));+    check!(+        b,+        "swx_stop did not stop it: the run stops for memory at the recycle, naming swx_stop and its answer",+        r["stoppedFor"] == "memory" && r["stopped"]["recycleFailedAt"] == "swx_stop"+            && r["stopped"]["detail"]["stopped"] != true && r["recycled"] == json!([])+            && !r["results"].as_array().map(|a| a.iter().any(|x| x["name"].as_str().unwrap_or("").starts_with("T01"))).unwrap_or(true),+        s(&r)+    );+    check!(b, "...and nothing was terminated", m.nothing_terminated(), "terminated.jsonl exists");+}
tests/wiring.rs+25−1
@@ -1232,5 +1232,29 @@ fn the_sign_in_skill_is_shipped_and_carries_no_credential() {     assert!(install.contains("install_skill solidworks-sign-in") && install.contains(".claude/skills") && install.contains(".codex/skills"));     assert!(uninstall.contains("uninstall_skill solidworks-sign-in"));     let verify = std::fs::read_to_string(root.join("tools/verify-wiki.py")).unwrap();-    assert!(verify.contains("skill_count") && verify.contains("== 2"), "verify-wiki expects two skills");+    assert!(verify.contains("skill_count") && verify.contains("== 3"), "verify-wiki expects three skills");+}++/// THE MEMORY SKILL (0.10.5) — the third shipped skill: packaged, installed,+/// removed, counted by verify-wiki, and pointed at from the main skill. It+/// carries no address or credential, keeps its parent line, and marks what is+/// not yet measured.+#[test]+fn the_memory_skill_is_shipped_as_the_third_skill() {+    let root = Path::new(env!("CARGO_MANIFEST_DIR"));+    let skill = std::fs::read_to_string(root.join("skills/solidworks-memory/SKILL.md")).expect("the skill");+    assert!(skill.contains("name: solidworks-memory") && skill.contains("Parent skill: **solidworks-bridge**"));+    assert!(skill.contains("[measured]") && skill.contains("[unmeasured"), "every claim marked measured or unmeasured");+    assert!(skill.contains("Never close the user's applications"), "the rule that matters most");+    assert_eq!(skill.chars().filter(|c| *c == '@').count(), 0, "an address-like '@' in the memory skill");+    let pkg: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(root.join("package.json")).unwrap()).unwrap();+    let files: Vec<&str> = pkg["files"].as_array().unwrap().iter().filter_map(|f| f.as_str()).collect();+    assert!(files.contains(&"skills/solidworks-memory/SKILL.md"), "listed in package.json files");+    assert_eq!(files.iter().filter(|f| f.ends_with("SKILL.md")).count(), 3, "three skills in the package: {files:?}");+    let install = std::fs::read_to_string(root.join("install.sh")).unwrap();+    let uninstall = std::fs::read_to_string(root.join("uninstall.sh")).unwrap();+    assert!(install.contains("install_skill solidworks-memory \"$H/skills/solidworks-memory\""));+    assert!(uninstall.contains("uninstall_skill solidworks-memory"));+    let main = std::fs::read_to_string(root.join("SKILL.md")).unwrap();+    assert!(main.contains("Long runs: read swx_status.memory first; see the solidworks-memory skill.")); }
tools/dsl-live-check.py+32
@@ -212,6 +212,38 @@ def checks():          # "spandrel" (square corner minus quarter disc), area (1 - pi/4) r^2, swept round the axis;          # Pappus with its centroid r(5/6 - pi/4)/(1 - pi/4) in from the wall (Monte Carlo agrees).          {"volume": fillet_cylinder_volume(10, 10, 1)}),+        ("T33", "REVERSED boss: extrude reverse:true from Top Plane goes -Y (Michael's own workaround style)", "#1.2",+         [{"op": "new_part"}, {"op": "sketch_rect", "plane": "Top Plane", "widthMm": 20, "heightMm": 20, "anchor": "center"},+          {"op": "extrude", "depthMm": 10, "reverse": True}],+         {"volume": 4000.0, "bbox": box(-10, 10, -10, 0, -10, 10)}),+        ("T34", "boss throughAll (one way) from a plane below the block runs up through all of it", "#1.2",+         BASE + [{"op": "sketch", "plane": {"offsetFrom": "Top Plane", "offsetMm": -5}, "entities": [{"circle": {"centerMm": [0, 0], "diameterMm": 4}}]},+                 {"op": "extrude", "throughAll": True}],+         {"volume": 4000 + cyl(4, 5), "bodyCount": 1, "bbox": box(-10, 10, -5, 10, -10, 10)}),+        ("T35", "Right Plane axes: sketch +X is model -Z, +Y is model +Y, extrude +X", "#1.5",+         [{"op": "new_part"}, {"op": "sketch_rect", "plane": "Right Plane", "xMm": 0, "yMm": 0, "widthMm": 10, "heightMm": 20},+          {"op": "extrude", "depthMm": 5}],+         {"volume": 1000.0, "bbox": box(0, 5, 0, 20, -10, 0)}),+        ("T36", "sketch on a SIDE face (+X) extrudes outward along +X (face sketch keeps Right Plane axes: origin projects to model Y=0)", "#1.1",+         BASE + [{"op": "sketch", "face": {"atPointMm": [10, 5, 0]}, "entities": [{"circle": {"centerMm": [0, 5], "diameterMm": 4}}]},+                 {"op": "extrude", "depthMm": 3}],+         {"volume": 4000 + cyl(4, 3), "bodyCount": 1, "bbox": box(-10, 13, 0, 10, -10, 10)}),+        ("T37", "sketch on the -Z face extrudes outward along -Z", "#1.1",+         BASE + [{"op": "sketch", "face": {"atPointMm": [0, 5, -10]}, "entities": [{"circle": {"centerMm": [0, 5], "diameterMm": 4}}]},+                 {"op": "extrude", "depthMm": 3}],+         {"volume": 4000 + cyl(4, 3), "bodyCount": 1, "bbox": box(-10, 10, 0, 10, -13, 10)}),+        ("T38", "a plane offset +12 mm from FRONT Plane (separate body at z 12..14)", "#1.1",+         BASE + [{"op": "sketch", "plane": {"offsetFrom": "Front Plane", "offsetMm": 12}, "entities": [{"rect": {"widthMm": 4, "heightMm": 4, "anchor": "center"}}]},+                 {"op": "extrude", "depthMm": 2}],+         {"volume": 4000 + 32.0, "bodyCount": 2, "bodyBoxes": [BASE_BOX, box(-2, 2, -2, 2, 12, 14)]}),+        ("T39", "linear pattern in TWO directions (X +12, Z -12): 4 bosses, all on the block", "#1.6",+         BASE + [{"op": "sketch", "face": {"atPointMm": [0, 10, 0]}, "entities": [{"circle": {"centerMm": [-6, -6], "diameterMm": 3}}]},+                 {"op": "extrude", "depthMm": 3},+                 {"op": "linear_pattern", "feature": "Boss-Extrude2", "directions": [{"axis": "x", "count": 2, "spacingMm": 12}, {"axis": "z", "count": 2, "spacingMm": -12}]}],+         {"volume": 4000 + 4 * cyl(3, 3), "bodyCount": 1, "bbox": box(-10, 10, 0, 13, -10, 10)}),+        ("T42", "shell OUTWARD 1 mm, top face removed: the original block becomes the cavity", "#1.6",+         BASE + [{"op": "shell", "thicknessMm": 1, "outward": True, "removeFacesAtPointsMm": [[0, 10, 0]]}],+         {"volume": 22 * 11 * 22 - 4000.0, "bbox": box(-11, 11, -1, 10, -11, 11)}),         ("T24", "the LEGO 2x4 acceptance brick (examples/lego-2x4.json)", "#1 acceptance",          lego["script"],          {"volume": lego_volume, "bbox": box(-15.9, 15.9, 0, 11.3, -7.9, 7.9), "appearance": [200, 0, 0],
tools/verify-wiki.py+3−2
@@ -46,8 +46,9 @@ def main():     tags = json.loads(tags) if isinstance(tags, str) else tags     check(sorted(tags or []) == sorted(pkg.get("tags", [])), "the tags", str(tags))     check(page.get("version") == ver, "the page's package version", str(page.get("version")))-    # Two user skills: solidworks-bridge and the dependent solidworks-sign-in.-    check(page.get("skill_count") == 2, "the package ships 2 skills", str(page.get("skill_count")))+    # Three user skills: solidworks-bridge and the dependent solidworks-sign-in+    # and solidworks-memory.+    check(page.get("skill_count") == 3, "the package ships 3 skills", str(page.get("skill_count")))     bv = get(API + "/files/BRIDGE_VERSION", raw=True).decode().strip()     check(bv == ver, "the page's BRIDGE_VERSION", bv)     want_title = os.environ.get("RELEASE_TITLE")
uninstall.sh+1
@@ -7,4 +7,5 @@ uninstall_skill() { # <name>   for D in "$HOME/.claude/skills" "$HOME/.codex/skills"; do rm -rf "$D/$1"; done } uninstall_skill solidworks-sign-in+uninstall_skill solidworks-memory echo "[solidworks-bridge] skills removed."