← Commit history

docs: 0.5.5 live degrade proof + 0.5.6 drain closes by path (a title is not an identity); ecosystem rows 37/43

Drew Owens ·3f4e759b92 ·1mo ago ·parent 8a49dd7
4 files changed +106−3
README.md+35−2
@@ -269,7 +269,40 @@ not create, and it never launches or stops SOLIDWORKS.  ## Roadmap -- v0.5.4 (this): **adversary round 8 — the anchor was right, its failure was+- v0.5.6 (this): **a title is not an identity — and the drain's CLOSE was the one+  place that rule was missing.** Victims were chosen by PATH and then closed with+  `CloseDoc(title)`, which resolves by name and does not prompt: on Drew's live+  session, leaked temps titled `First.step-1.SLDPRT` sit beside his SAVED+  `…\validation\First.step-1.SLDPRT`, so SOLIDWORKS would have chosen which one+  died while the response recorded the temp as closed either way. The 254 green+  tests said nothing about it because the mock removed documents by a unique+  internal key. Now: the victim is closed by its full path; any victim whose title+  another OPEN document shares is refused and reported in `titleCollisions` with+  every path that shares it; every close is verified with+  `GetOpenDocumentByName`, and a close that reports success while the document is+  still open stops the drain and fails loudly as `wrong_document_closed`. The mock+  models `CloseDoc`'s by-name resolution, so a close-by-title now fails the suite.+  A walk that ended early (a `GetNext` on an invalidated iteration) is no longer+  reported as complete: `walkComplete` / `walkStopped` are measured and `done`+  requires reaching the end. 257 tests.+- v0.5.5: **the degrade path was itself an unbounded marshal.** Found on+  the live 2144-document session the hour 0.5.4 was deployed: `swx_docs` still+  timed out at 30 s and the com host was killed, while `GetDocumentCount`+  answered in 0.7 s. Above the cap the code called `GetDocuments` — one call that+  marshals every handle — and then `GetPathName` on all 2144 of them to fill a+  field named `importTempsInSample`. The drain had the same shape, so it would+  have wedged on the very session it exists to drain. Now everything walks:+  `GetFirstDocument()`/`GetNext()`, one document at a time, bounded by a count and+  a time box; the sample costs the sample; the drain scans a bounded window and+  reports `documentsScanned`; `documents()` refuses above the cap rather than+  letting a caller find the wedge; `close_all_documents` still closes a flooded+  session but reports counts with null lists instead of empty ones. And the mock+  can now be SLOW (`SWX_MOCK_DOC_LATENCY_MS`, `SWX_MOCK_OPEN_DOCS`), because the+  reason every test passed is that its enumeration was instant — the regression+  test holds the live shape: 2144 documents, cap 200, 15 ms each, `swx_docs` in+  under 3 s. Ecosystem row 37 is marked *mock-proven; failed live; re-proof+  pending* until it is re-run on Drew's session. 254 tests.+- v0.5.4: **adversary round 8 — the anchor was right, its failure was   silent.** The drain compares a path SOLIDWORKS reported against a `%TEMP%` this   process resolved, and the two can legitimately differ (ab may run under a   service profile or a redirected TEMP, and the leaked temps were written by an@@ -288,7 +321,7 @@ not create, and it never launches or stops SOLIDWORKS.   The 8.3 rule is now exact rather than merely tight: the mangled form needs its   six-character head, the COLLISION form (`DREW~1` for `drew`, which is what a   domain-joined machine really produces beside `drew.ADOM`) needs the whole name,-  and a loose prefix is rejected. 250 tests.+  and a loose prefix is rejected. - v0.5.3: **adversary round 7 — the once-swept precondition, the   one-sample exit, and the anchor that undid itself.** The drain's `%TEMP%` anchor   was OR-ed with a shape test, which made it advisory: any path whose tail read
SKILL.md+13
@@ -293,6 +293,19 @@ adom-bridge --ai-thread "<thread>" swx_save_as '{"outputPath":"C:\\parts\\bracke   re-measured before EVERY rung, so a multi-rung ladder can refuse in the middle   (`refusedMidLadder:true`) if a document appears while a rung is settling, and it   never continues against a different process (`stop_identity_changed`).+- **On a flooded session, every list is a SAMPLE and says so.** Above the+  enumeration cap `swx_docs` returns `{count, truncated:true, sample,+  importTempsInSample, importTempsInSampleOf}` and NO `docs` list — it walks ten+  documents, it does not enumerate thousands (doing that killed the com host on a+  live 2144-document session). `swx_close_all_documents` on such a session still+  closes, but reports `closed`/`remaining` as **null** with counts, and names the+  unknown in `skipped`. Never read a null list as "nothing there".+- **The drain never closes a document whose title another open one shares.** It+  closes by full path and verifies each close, but `CloseDoc` resolves by name — so+  a shared title is refused and reported in `titleCollisions` (with every path that+  shares it) rather than letting SOLIDWORKS pick. The user decides: close it+  themselves, or save the other document under another name. `done:false` while any+  collision stands. - **`swx_close_import_temps` needs a resolved `%TEMP%`, and says so.** The drain   identifies a temp by its LOCATION under this user's resolved temp directory —   that path is the whole proof, because the close does not prompt. A path that
docs/ECOSYSTEM-TABLE.md+1−1
@@ -108,7 +108,7 @@ templates**, alisamsam **22**. | 40 | **Crash-reporter recovery** (click "Restart SOLIDWORKS Design" once, structured match, ledgered) | ✗ | **shipped, DISABLED by default** until the real control-tree reply is captured | ✗ | ✗ | ✗ | ✗ | ✗ | | 41 | **COM retry-on-reject** (busy host: RPC_E_CALL_REJECTED / RETRYLATER backoff on every call) | ? | **✓ live 0.3.3** — 50-read smoke gate, 0 under-reporting, 11 rejects absorbed | ✗ | ✗ | ✗ | ✗ | ✗ | | 42 | **Real-work proof** (multi-assembly build + BOM exported from the assembly, verified against a packet) | ✓ (electronics) | **✓ live** — NSF crate: 5 assemblies, 383 placements, 32-row BOM, 28/28 part numbers match | ✗ | ✗ | ✗ | ✗ | ✗ |-| 43 | **Import / resource lifecycle — no leaked temps, drainable, enumeration never wedges** | ✗ | **✓ 0.5.0** — enumeration degrades above a cap (`{count,truncated,sample}`, never a 30s timeout+kill); `swx_close_import_temps` drains STEP/IGES `…\Temp\swx<pid>\IC~~\…` temps in the DEFAULT posture (path is the ownership proof, survives a pid change); import asserts a net +1. Root-caused a live 1342-doc / 8 GB leak. **0.5.3**: the resolved `%TEMP%` is the WHOLE location proof (8.3-aware, no shape-test fallback), and with no resolved root the drain refuses (`temp_root_unknown`) instead of closing nothing quietly. **0.5.4**: the report says which root it compared against (`tempRoot`) and counts documents that carry the launcher's tail but sit elsewhere (`importTempsElsewhere` + a sample), so a drain that matched nothing can no longer claim it drained everything — the mismatch case is real, because ab's `%TEMP%` need not be the one SOLIDWORKS used | ✗ | ✗ | ✗ | ✗ | ✗ |+| 43 | **Import / resource lifecycle — no leaked temps, drainable, enumeration never wedges** | ✗ | **✓ 0.5.0** — enumeration degrades above a cap (`{count,truncated,sample}`, never a 30s timeout+kill); `swx_close_import_temps` drains STEP/IGES `…\Temp\swx<pid>\IC~~\…` temps in the DEFAULT posture (path is the ownership proof, survives a pid change); import asserts a net +1. Root-caused a live 1342-doc / 8 GB leak. **0.5.3**: the resolved `%TEMP%` is the WHOLE location proof (8.3-aware, no shape-test fallback), and with no resolved root the drain refuses (`temp_root_unknown`) instead of closing nothing quietly. **0.5.4**: the report says which root it compared against (`tempRoot`) and counts documents that carry the launcher's tail but sit elsewhere (`importTempsElsewhere` + a sample), so a drain that matched nothing can no longer claim it drained everything. **0.5.6**: the victim is CLOSED by the path it was chosen by (it was closed by TITLE, which `CloseDoc` resolves by name without prompting — a live session held temps sharing a saved file's title), a shared title is refused and reported, and every close is verified by path — the mismatch case is real, because ab's `%TEMP%` need not be the one SOLIDWORKS used | ✗ | ✗ | ✗ | ✗ | ✗ | | 44 | **Graceful QUIT that is measured, not assumed** (never a kill, never answers a dialog, proves the exit from the process census) | ✗ | **⚠ 0.5.3 mock-proven, UNPROVEN LIVE** — a rung ladder (a `ExitApp` → f Restart Manager *without* `RmForceShutdown`), pinned at rung (a); 7 re-measured preconditions where UNKNOWN refuses and `force` waives the UNSAVED question ONLY; every COM reference released before and after each rung, with outstanding refs named in the report; success = *this pid AND this creation time* absent from the census, never `GetActiveObject`; per-session children reaped while the documented platform daemons are left alone; a dialog stops the ladder permanently and is never answered; nothing disowned until the exit is verified. every precondition re-measured before EVERY rung (a mid-ladder refusal is a real outcome), the exit POLLED to the rung's budget rather than sampled once (a real `ExitApp` returns before the process goes), the dialog census covering the `swCefSubProc` web-view children by parentage, the count cross-checked against the enumeration, and the message rungs re-verifying window ownership before they send. `tools/quit-gate.py` proves the session was bridge-launched and has not been run | ✗ | ✗ | ✗ | ✗ | ✗ |  *(42 numbered rows; rows 29–31 are the N/A block that accounts for most of the Fusion verb-count gap. Rows 36–42 added 2026-09-08 from the crate build and the launch-resilience rounds — the bar is "significantly more powerful than the MCP servers", so every robustness capability gets a row with its evidence level, not a slogan.)*
docs/IMPORT-LEAK.md+57
@@ -323,3 +323,60 @@ 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:++```rust+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.