main
Drew Owens 0.9.0: bridge data moved out of the install cache (recovery survives updates), closed-in-place records, gates route thr… 5d02ca9 20d ago

The STEP-import document leak (found 2026-09-08)

Symptom

swx_status reported 1342 open documents; GetDocumentCount() (authoritative COM) confirmed 1342; swx_docs enumeration timed out at 30s (that many documents cannot be marshalled in the budget); SOLIDWORKS private memory sat at 8.1 GB on an otherwise idle, responding session.

Cause (corrected after reading cmd_import)

swx_import is NOT itself leaking: it correctly leaves ONE open, unsaved, bridge-created document, the same contract as swx_new_document. The 1342 are the same two fixtures imported over and over and never closed — note the -1/-2 title suffixes SOLIDWORKS adds when a prior import of the same name is still open. Source fixtures First.step/Second.step are not in the current tree, so this is an earlier test/run loop that imported without closing. Every open document was a STEP-import temporary:

title: First.step.SLDPRT   Second.step.SLDPRT   First.step-1.SLDPRT ...
path : C:\Users\drew\AppData\Local\Temp\swx107688\IC~~\First.step.SLDPRT

swx<pid>\IC~~\ is the folder SOLIDWORKS writes when converting a STEP. These docs are unsaved (temp path = never saved to a real location). They accumulated because callers imported without closing AND — the actual bridge defect — before the 0.4.4 ownership fix a host respawn disowned them (bug #12), so nothing could drain them in the default posture even if it tried. The count is static and no loop is running, so it is historical, not active, and never drains except by restarting SOLIDWORKS.

Why it matters

  • This is the real root of the crate-build memory exhaustion (0.5 GB → 13 GB across the build, then a hang at 17.4 GB / "52 docs"). The 7 fastener imports plus repeated import-based tests each leaked a loaded document; "openDocs" was never really the crate assemblies alone.
  • It quietly defeats the quit ladder's precondition ("verified Some(0) documents") and makes swx_docs time out, which earlier read as the silent-empty bug.
  • No MCP server in the ecosystem manages this; getting resource lifecycle right is exactly the robustness edge Drew asked for.

The real bridge defects (0.5.0), in order of impact

  1. swx_docs / count enumeration must degrade, never time out. Read GetDocumentCount first; above a cap, page the enumeration or return {count, truncated:true, sample:[...], _hint:"N documents open; enumeration exceeds budget — likely undrained import temps, run swx_close_import_temps"} instead of blocking 30s and killing the com host. swx_docs must not time out on a large count: read GetDocumentCount first; above a cap, page the enumeration or return {count, truncated:true, sample:[...], _hint:"N documents open; enumeration exceeds budget — likely leaked import temps, see swx_purge_imports"} rather than killing the com host.
  2. A drain verb. swx_close_import_temps (gated, bridge-owned only): close every open document whose path is under %TEMP%\swx<pid>\IC~~\, by title/path, reporting count freed and memory reclaimed. These are unambiguously transient and bridge-created; never a user document.
  3. Add these IC~~ temps to the ownership model so the default posture can close them (0.4.4 already makes bridge-created docs survivable across respawns; confirm import temps are tracked as created).
  4. Import net-count assertion. swx_import should verify GetDocumentCount rose by exactly 1 and report the resulting doc's title/path, so a caller can close it — and should carry an _hint reminding that an imported doc stays open until closed or saved, like new_document.
  5. NOT a change to swx_import's core contract — it correctly returns one open unsaved doc.

Cleanup of the current 1342

They clear on the next SOLIDWORKS restart (which the launch gate needs anyway). No user document is among them (all are %TEMP%\swx107688\IC~~\), but closing 1342 live is slow and the enumeration times out, so a restart is the clean path.

Shipped in 0.5.0 (Workstream A)

All four defects are closed; swx_import's contract is unchanged (it still returns one open, unsaved, bridge-created document, like swx_new_document).

  1. Enumeration degrades, never wedges. swx_docs and app_state read GetDocumentCount first (one COM call, always answers). Above host::doc_enum_cap() (200; SWX_DOC_ENUM_CAP overrides it for tests) they return {count, truncated:true, sample:[first 10 titles/paths], importTempsInSample, _hint:"…run swx_close_import_temps"} instead of marshalling 1342 handles for 30s and getting the com host killed. app_state also sets documentsTruncated, and the server's ownership prune is SKIPPED when truncated (prune_unless_truncated) — an empty "unlistable" set must not disown every document the bridge created (bug #5's shape, reached through the leak).
  2. swx_close_import_temps (new, gated with a reason, NO write opt-in): closes every open document whose path matches is_import_temp — …\Temp\swx<pid>\IC~~\…. The path is the ownership proof and is stronger than the registry: it survives the pid change that made the 1342 (from a dead bridge process) un-ownable, and no user document can match it. Drains in bounded batches (close_import_temps {max} closes a batch and reports done; the verb loops to a wall budget), so neither a single com call nor the verb runs unbounded on a huge count.
  3. Import net-count assertion. cmd_import counts via GetDocumentCount (not the enumeration, which would itself time out at 1342), asserts the count rose by exactly one, and returns documentsOpened / netCountOk / documentCount plus a hint that the document stays open until closed or saved.
  4. Ecosystem row 43 — "import / resource lifecycle: no leaked temps, drainable, enumeration never wedges." We are alone.

Tests: tests/import_lifecycle.rs (5) — net-count assertion, degrade-above-cap (swx_docs stays fast + truncated + sampled), drain in the default posture (never touches a new_document), bounded batching (done:false then finish), and drain-after-a-com-host-respawn (the temps are identified by path, not by a registry the respawn cleared). Plus coverage in the e2e verb walk.

The live 1342 still clear only on the next SOLIDWORKS restart (the launch gate needs one anyway); swx_close_import_temps is what prevents the next pile.

Adversary round on 0.5.0 → shipped in 0.5.1

Four findings, one of them data loss in the DEFAULT posture.

F1 (HIGH — data loss). is_import_temp proved the wrong thing. It asked only that \temp\swx and \ic~~\ each appear SOMEWHERE in the path, in any order, in any folders. So a user's own saved file qualified — D:\Temp\swx-archive\2026\IC~~\customer-final.sldprt, …\Temp\swxnotes\IC~~\keep.sldprt, or anything with an IC~~ folder ABOVE a Temp\swx… one — and cmd_close_import_temps closes what qualifies with CloseDoc, which does not prompt.

It is now a SEGMENT WALK requiring the launcher's actual contiguous layout: a Temp segment, then immediately a swx<digits> segment (the pid; the 8.3 short form SWX107~1 counts), then an IC~~ segment BELOW it, then a file. Case-insensitive, either slash. The pid VALUE is deliberately not checked — the 1342 leaked temps belonged to a dead bridge process, and scoping the drain to "documents open in the session we are attached to" is what makes it useful.

F2 (MEDIUM). An unreadable count fell through to the full enumeration — count.map(..).unwrap_or(false) — which is the 30s wedge the cap exists to prevent, arriving exactly when the session is under the memory pressure that made the count unreadable. None is now UNKNOWN-and-truncated: {count:null, truncated:true} with a hint saying null is not zero, and no marshal is attempted. app_state the same, so ownership is not pruned against it either. And a verb that cannot resolve its target now refuses with document_list_unavailable instead of the confident wrong "no open document is titled X".

F3 (MEDIUM). The mock reported path:"" for a temp while the real GetPathName returns the IC~~ path, and the mock drain matched a FLAG while the real one matched the PATH — so is_import_temp, the write gate's exists-check, save_as and path-keyed ownership were never exercised on a realistic temp path. F1's subject had no e2e test. The mock now reports the synthetic temp path everywhere GetPathName would (doc_info and reported_path) and its drain matches through the same is_import_temp.

Making the mock honest immediately exposed three real defects it had been hiding, all of which existed on the live machine:

  • the com host's write veto refused close_import_temps outright (its ownership check resolved a target and found it foreign), so the drain could never run in the default posture — the whole point of the verb. It is now an explicit, justified exemption on the host's allowlist, which is why F1's tightening matters: the path proof is the only guard left.
  • swx_import recorded only the TITLE in the ownership registry while the open document reported a path, so the two never matched and the bridge refused to close the document it had just imported. It now records the path — through a new remember_scratch_at, not rename_scratch, because rename_scratch finds its target by title and would have reassigned the recorded path of a different owned document sharing the imported title (SOLIDWORKS titles an imported STEP <stem>.SLDPRT, so importing beside a saved part of that name is ordinary).
  • the mock's host_owns branched on saved, so an unsaved-but-pathed temp was looked up as pathless. It now keys on the path SOLIDWORKS would report, which is what the real backend has always done.

F4 (LOW). The ActiveDoc recovery branch can report another concurrent import's title/path. No behaviour change: it now sets titleMayBeAnotherConcurrentImport:true and says so in the hint.

Adversary rounds 6 and 7 → the anchor becomes the whole proof (0.5.2, 0.5.3)

Round 6, D1 (HIGH, data loss). The proof was still only about SHAPE: two tokens in a path, so a user's own saved file under folders they had named (…\Temp\swx-archive\2026\IC~~\customer-final.sldprt) qualified for a CloseDoc with no save prompt. 0.5.2 replaced it with a segment walk requiring the launcher's contiguous layout — <TEMP>\swx<digits>\IC~~\<file>, IC~~ as a folder and not a file, swx followed by DIGITS (not swxnotes) — and anchored it to the resolved %TEMP%.

Round 7, D-1 (HIGH, data loss). The anchor had a fallback that undid it:

matches_resolved || canonical      // canonical = the parent ends appdata\local\temp

The canonical arm made the anchor advisory. Any path whose tail read …\AppData\Local\Temp\swx<digits>\IC~~\<file> qualified wherever it lived — a restored profile backup on another volume (E:\Backups\drew\AppData\Local\Temp\…), another user's profile, a copy somebody took of a leaked temp they wanted to keep. And because matches_resolved was unwrap_or(false), when %TEMP% could not be read the shape test was the ONLY test: the one place in this bridge where an unreadable precondition guessed instead of refusing, authorising a close with no save prompt.

0.5.3:

  • the resolved %TEMP% is the whole location test. The canonical arm is deleted. The pid folder's parent must BE the resolved temp directory, segment for segment.
  • both sides are resolved, not merely compared. GetPathName can hand back C:\Users\DREW~1\AppData\Local\Temp\… for the very directory %TEMP% reports as C:\Users\drew\AppData\Local\Temp, so segments match when they are equal or one is the other's 8.3 short form — by the real rule (six characters of the name with spaces and periods removed, plus ~n), so a~1 cannot stand in for adversary. GetLongPathName is deliberately not used: it answers only for a path that still exists on disk, and this rule is asked about documents whose temp file may already be deleted while the document is still open.
  • no root, no drain. temp_root() == None matches nothing, and swx_close_import_temps refuses with temp_root_unknown rather than reporting closed:0, done:true — which, on a session holding hundreds of leaked temps, is a lie a caller acts on.
  • the rule is now testable off Windows. SWX_TEMP_ROOT names the resolved root, and in mock mode temp_root() answers with the simulated launcher's C:\Users\drew\AppData\Local\Temp instead of this host's /tmp — because the suite runs on Linux, and a location rule that cannot be exercised here is the one that has now twice been the data-loss rule. The fault file mock-temp-root-unknown models an unreadable root so the refusal has a test.

Adversary round 8 → the anchor was right, its FAILURE was silent (0.5.4)

F (MEDIUM, wrong report — not wrong action). temp_root() resolved the BRIDGE process's %TEMP%, and the drain then required the document's path to sit exactly under it. That is the correct rule, and it closed the data-loss hole. But the two directories can legitimately differ: the com host inherits Adom Bridge's environment, and ab may run under a service profile, a redirected TEMP or a different account from the interactive session SOLIDWORKS lives in — and the 1342 temps this verb exists for were written by a different, dead bridge process, whose %TEMP% 0.5.3 simply assumed was ours.

On a mismatch the drain matched nothing and answered:

{"success": true, "closed": 0, "done": true,
 "_hint": "Every open STEP/IGES import temporary was closed."}

with three of them open. Nothing was wrongly closed — the miss is the safe direction for the CLOSE — but the REPORT was a confident wrong claim about the user's session, and an operator running launch → drain → quit would have read step 2 as finished and been surprised by the quit gate refusing on open documents.

0.5.4 makes the failure loud instead of silent:

  • tempRoot and tempRoots are in the response, so a mismatch is diagnosable from the output on a machine where nobody can attach a debugger.
  • near misses are counted. has_import_temp_tail recognises the launcher's own …\swx<digits>\IC~~\<file> tail wherever it lives; documents that carry it and fail the root test are reported as importTempsElsewhere: N with an importTempsElsewhereSample path.
  • a matched-nothing drain does not claim completion. With N > 0 and closed == 0 the verb reports done: false and a hint that says what happened, why nothing was closed (the resolved temp directory is the whole proof, and the close does not prompt), and what to do: read the sample path and restart the bridge with SWX_TEMP_ROOT set to the directory above the swx<pid> folder, or close them with swx_close_all_safely, which writes every document to a recovery folder and verifies it before closing. Never ask the user to close documents — Drew, 2026-09-11: "I despise every time you ask for help closing."
  • two candidate roots, with a documented precedence. SWX_TEMP_ROOT, when set, is the whole set. Otherwise the set is this process's %TEMP% (TEMP, TMP, TMPDIR) and the interactive user's HKCU\Environment\TEMP expanded, each included only if readable; a document under either qualifies, and an empty set still refuses with temp_root_unknown. Reading that registry value is the least invasive way to learn the temp directory of the session SOLIDWORKS is in: its own GetTempPath cannot be called from another process, and reading its PEB for its environment block is invasive, version-fragile and needs rights this bridge should not want.

One note answered differently from how it was raised. Pass 8 also observed that accepting drew~1 as the 8.3 form of drew is a widening, since Windows does not mangle a name that already fits 8.3, and suggested restricting the tilde form to segments longer than 8 characters or containing a space or period. That is right about mangling and wrong about the other case Windows generates a tilde name for: a collision. When two entries in a directory claim the same short name, the short name of the first becomes the whole name plus ~n — and the standard example is a profile directory on a domain-joined machine, where C:\Users\drew sits beside C:\Users\drew.ADOM and the short name of drew really is DREW~1. Restricting to the mangled form would make the drain MISS real leaked temps on exactly the machine class this bridge ships to, which is the wrong direction for a verb that exists because 1342 temps went unclosed. So the rule is now exact rather than merely tighter: the mangled form requires the six-character head (ADVERS~1 for adversary, so a~1 is rejected), the collision form requires the whole name (DREW~1 for drew), and neither admits a loose prefix — which was the actual looseness worth removing. Pass 7's positive control stays green because the case it pins is real.

LIVE, 2026-09-09 — the degrade path was itself an unbounded marshal (0.5.5)

0.5.4 was deployed to the dev bridge and swx_docs was run against Drew's real session: 2144 documents open, and it still timed out at 30 s and the com host was killed. GetDocumentCount had answered in 0.7 s.

The cap was doing its job and the code under it was not. Above the cap, cmd_docs did this:

let all = sw.call("GetDocuments", &[])?.as_array();   // EVERY handle, one call
let sample = all.iter().take(DOC_SAMPLE)…             // …to look at ten
let temps  = all.iter().filter(|d| GetPathName…)      // …then 2144 round trips

GetDocuments marshals the whole array in a single call and did not return within the budget; the temps loop then added one GetPathName per document — 2144 more round trips — to compute a number reported in a field literally named importTempsInSample. The mock's enumeration was instant, so every test passed and ecosystem row 37's "degrades honestly instead of timing out" was mock-proven and false live. The same shape was in cmd_close_import_temps: the drain would have wedged on the exact session it exists to drain.

0.5.5:

  • walk_documents — GetFirstDocument() → GetNext(), one document at a time, bounded by a count AND a time box. Measured live the day before on the 1342-document session: $sw.GetFirstDocument().GetNext().GetTitle() answered immediately. Cost is now O(what we look at), never O(what is open).
  • cmd_docs above the cap walks DOC_SAMPLE documents and nothing more. importTempsInSample is counted over the sample only — as its name always said — and reports importTempsInSampleOf so a bare number cannot read as a session count. A walk stopped by its 5 s budget says sampleTruncatedByTime:true.
  • The drain walks too, in two phases: collect up to max victims from a bounded window (about 4× the batch, 20 s budget), then close them by title — closing while walking would invalidate the iteration. It reports documentsScanned and walkTruncatedByTime, so importTempsElsewhere is readable as a count over the window rather than over the session.
  • documents() refuses above the cap (docs_enumeration_capped) instead of letting any caller discover the wedge the hard way. close_all_documents gets a capped branch that still performs the close — one COM call — and reports counts with closed/remaining/wouldPrompt as null, never as empty lists, with the server turning that into an explicit UNKNOWN entry in skipped. app_state and check_recovery already read the count only.
  • The mock can now be slow. SWX_MOCK_DOC_LATENCY_MS charges per document touched, at exactly the points the real backend pays a round trip, and SWX_MOCK_OPEN_DOCS floods a session. The regression test holds the live shape: 2144 documents, cap 200, 15 ms/document — 32 s of simulated cost if anything marshals the session — and requires swx_docs to answer in under 3 s with truncated:true, the drain to close a bounded batch without scanning 2144, and app_state to degrade the same way.

The lesson is the one this project keeps relearning, in a new place: a mock that cannot exhibit the failure cannot prove the fix. The cap was tested; the cost of the code under the cap was not, because nothing in the mock had a cost at all.

Adversary round 9 → a title is not an identity, and the drain's CLOSE was the one place the rule was missing (0.5.6)

The rule has been enforced everywhere else in this bridge for four releases: resolution puts path before docName, swx_docs refuses an ambiguous title with ambiguous_target, ownership rows are keyed on the path when there is one, same_path exists for exactly this reason. The drain broke it at the last step:

let mut victims: Vec<(String, String)> = …;              // chosen BY PATH
for (title, path) in victims {
    if sw.call("CloseDoc", &[&Var::bstr(&title)]).is_ok() // …closed BY TITLE

ISldWorks::CloseDoc resolves a document by name and does not prompt. SOLIDWORKS titles are not unique across paths, and Drew's live session holds exactly the collision that matters: leaked conversion temporaries titled First.step-1.SLDPRT open beside his saved C:\Users\drew\validation\First.step-1.SLDPRT. Asking by that title lets SOLIDWORKS choose which one dies — and the response would have recorded the temp's path as closed either way.

The 254 green tests were not evidence about the close key: the mock's drain removed the document by its own unique internal key, so it always closed exactly the right one. Same failure of the mock as pass 8 (the root always agreed) and pass 5's F3 (the path was always empty), in a new place.

0.5.6:

  • the victim is closed by the identity it was chosen by — its full path. For an import temporary GetPathName IS the IC~~ path, which is what qualified it in the first place. Never the bare title, here or in close_doc (which passes the path whenever the document has one; resolve already refused an ambiguous title before it).
  • a title collision is refused, not resolved. Every document the walk passes contributes (title, path) — the title is read for all of them, not just the victims, which is one extra call per document inside an already bounded window — and any victim whose title another open document shares is skipped and reported as titleCollisions: [{title, paths}]. The hint says what only the user can decide: close it yourself, or save the other document under another name. The scan deliberately does not stop at max, because stopping it would blind the collision check to a document later in the window.
  • every close is verified by path. GetOpenDocumentByName(path) is one call: after a close that reported success, the document must be gone. If it is still open then something else was closed, and the drain stops immediately and reports wrong_document_closed as an error with everything measured. That branch must be unreachable; the check is the proof rather than the hope.
  • the mock now models CloseDoc's by-name resolution — a full path matches only itself, a title resolves to the FIRST open document carrying it — so a_saved_document_sharing_a_temps_title_survives_the_drain is a real test instead of a pin, and a future close-by-title would fail the suite.
  • a walk that ended early is not reported as complete. walk_documents now returns WHY it stopped (reachedEnd / hitMax / timedOut / iterationFailed), because a GetNext that fails on an invalidated iteration — a document closed under us — is not the end of the session. walkComplete and walkStopped are in the response and done requires reachedEnd, no collisions and no failed closes.

Adversary round 10 → the close key is right; the WINDOW it reasons over was not (0.5.7)

Pass 10 confirmed the pass-9 fix at the mechanism (the fixture scenario — two temps plus a saved same-titled document, bridge-launched — is safe) and found that everything around the close still spoke as though it had seen the session.

  • done did not consult WalkStop::complete(). hit_limit was seeded from walk_stop == TimedOut alone, so a walk that stopped at its scan cap (hitMax) or on an invalidated iteration (iterationFailed) was treated as one that reached the end. Measured on the adversary's fixture: 55 of 60 documents scanned, a temporary sitting at #60 still open, done:true. It is now done = reachedEnd && scanned == documentCountBefore && no collisions && no failed closes && the batch limit was not hit && no root mismatch.
  • walkStop is published (reachedEnd / hitMax / timedOut / iterationFailed). It was computed, named, and never sent — so a caller could not tell those four apart, and titleCollisions: [] was uninterpretable.
  • The collision claim is scoped to the window it was actually checked over. titleCollisionsCheckedOver: documentsScanned, and the hint says so in words: collisions were checked among the N documents this call scanned, not across the session. A same-titled document beyond the window is invisible to the check — harmless for the close, because the path is the key, and load-bearing for the report.
  • The mock is window-bound now. It selected victims and checked collisions over the WHOLE session while reporting a 55-document scan, so it closed documents its own reported window never reached — which is why 257 green tests said nothing about any of the above. Victim selection, the collision check and the near-miss count all run over the walked window only.
  • The CloseDoc(path) premise is now discriminated, not assumed. The help text says only "name of the document to close"; that a full path is accepted has never been run live. If a close returns success and the document is still open, one extra count settles which of two very different things happened:
    • the count did not move → the call did nothing. That is a CAPABILITY failure: closeIgnoredPath:true, errorCode:"close_by_path_unsupported", the drain stops, and it does not fall back to a title — that is exactly the ambiguity round nine removed. An unreadable count reads as this case, because it is the one that accuses nobody.
    • the count dropped while our victim is still open → some other document was closed: wrong_document_closed, stop, report loudly. Still unreachable; still checked. The mock models the failing premise (mock-closedoc-ignores-path: the path form returns success and closes nothing), so the discriminator has a test.
  • The server forwards closedDocs (title + path for every document closed) and titleCollisions, not just counts. A caller needs to know WHICH files went.

tools/drain-gate.py is the live proof of the premise, on documents the bridge makes itself: import the same fixture twice, save one copy under the same title into a scratch dir, then ONE swx_close_import_temps {batch:1} and assert that exactly one document closed, that it was named by its IC~~ path, that the saved twin is still open, that the count dropped by exactly one, that the collision was reported, and that every document open at baseline is still open. It closes only what it created and removes its own scratch directory.

Adversary round 11 → the discriminator's third case, and the gate's own honesty (0.5.8)

The unreadable count took the benign arm. When CloseDoc(path) returned success and the document was still open, 0.5.7 asked whether the open-document count had moved — and an unreadable count fell into _ => true, meaning "the call did nothing": close_by_path_unsupported, a capability problem, nobody accused. But a count can become unreadable because something large just changed. Closing a big assembly is exactly the kind of event that makes GetDocumentCount stop answering, so the reassuring reading is the one this bridge may not take. There are three outcomes, not two:

after a close that "succeeded" while the victim is still open verdict
the count did not move the call did nothing — close_by_path_unsupported
the count DROPPED another document was closed — wrong_document_closed
the count cannot be read close_outcome_unknown, outcome:"undetermined"

The third stops the drain and says what it does not know: the victim's path, the count immediately before the close, and "a human must check whether this path, or another document, closed". It never claims nothing happened.

And it had no fixture. The mock read its count from self.docs.len(), so mock-doccount-none could not reach the drain and that arm was dead code as far as the suite was concerned — the arm that decides whether a user's document was closed. The mock now reads every count through one seam that honours the fault.

The gate's own claims got the same treatment (tools/drain-gate.py):

  • the PASS banner is conditional. With the saved twin never open it prints PARTIAL — the close key is proven, the collision half is untested — instead of a pass that covers something it did not exercise.
  • step 3 now tries the twin save in the default posture first, with no allowUserWrites, and records defaultGateAllowedScratchSave as a finding. A bridge-created document written to a non-existing path inside a scratch dir the gate made is exactly what the choke-point rule is supposed to allow, so a refusal there is a BRIDGE BUG the gate names rather than works around. It then retries with the opt-in so the rest of the run still happens; the finding stands either way. (It does not reproduce in the mock — the rule is now pinned by the_default_posture_lets_the_bridge_save_its_own_document_to_a_new_path, which also pins that overwriting an EXISTING file still refuses.)
  • cleanup deletes only the two files the gate wrote, by name, and removes the directory only if it is then empty. A recursive delete of a directory whose contents we did not write is the same class of act this bridge refuses everywhere else — and SOLIDWORKS puts its own artefacts in there.

Adversary round 12 → the write gate's own unanswerable question (0.5.9)

Not about the drain at all: about the choke point every write in this bridge goes through. The destination test was

let clobbers = destination.map(|d| !d.is_empty() && Path::new(d).exists());

and Path::exists() returns false for two different facts: nothing is there, and I could not look — a parent directory this process cannot traverse, a UNC share it has no token for, a device that is not ready, a path over the length limit. The gate read the second as "the destination is free" and allowed the write with no opt-in, no confirm and no reason.

It matters because the two processes do not share a filesystem view. The com host is a child of Adom Bridge; SOLIDWORKS runs in the user's interactive session with the user's network token. \\server\share\part.sldprt that the bridge cannot stat is a file SOLIDWORKS can open and overwrite — with the gate having already said yes. This was the last place in the bridge where an unanswerable question read as permission.

0.5.9 gives the question three answers, in one shared function (writes::destination_state) used by both enforcement layers:

symlink_metadata verdict
Ok(_) Occupied — overwriting needs the full opt-in
Err(NotFound) Free — a bridge-created document may be written there with no opt-in
any other Err Unverifiable — REFUSE, destination_unverifiable

symlink_metadata rather than try_exists deliberately: a dangling symlink IS something at that path, and writing through it lands somewhere the caller did not name. The refusal happens before the opt-in is consulted, and the hint says why: the opt-in answers "may I overwrite this?", and this is "I do not know what is there" — so the opt-in cannot waive it. Both layers are tested: the server's via swx_save_as, and the com host's by driving its stdio protocol directly with the opt-in fully granted, so nothing but its own veto can be what refuses.

And the sweep item, which is the same defect one layer down. The server read closeOutcomeUnknown with unwrap_or(false) — so a com host too old to send that field reported "the outcome was determined; nobody needs to look". The com host and the server are separate binaries and can be a mismatched pair (a half-applied update, a stale exe in a cache dir). Now: the ready handshake carries the host's version and a skew is an explicit boot error naming both versions, and the field itself is read as Option<bool> where absent beside a reported close failure is treated as undetermined.