main
Drew Owens 0.11.4 source 8889116 1d ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528
//! The v0.3 verbs — SOLIDWORKS' answer to the Fusion 360 bridge's surface.
//!
//! Arg names and response keys deliberately MIRROR `fusion_*` wherever the
//! concept survives the translation (`outputPath`, `filePath`, `name`,
//! `parameters`, `bodies`, `current`, `applied`, `actions`, `stages`,
//! `wouldPrompt`, `closed`, `remaining`), so an AI that already knows how to
//! drive Fusion can drive SOLIDWORKS without relearning the vocabulary. Where
//! the concept does NOT survive — Fusion's Python scripting host, its cloud file
//! store, its programmatic install — the verb says so in its own `_hint` rather
//! than pretending, and `docs/FUSION-PARITY-REPORT.md` carries the long form.
//!
//! Everything here that talks to SOLIDWORKS goes through the same com-host
//! child as v0.2; everything that talks to a WINDOW goes through ab's generic
//! desktop verbs (this bridge owns no window code).

use crate::verbs::{
    Target,
    arg_bool, arg_str, err, err_code, gate, ok, ok_merge, safe_out, sidecar_error, Ctx,
};
use serde_json::{json, Value};
use std::time::Duration;

const AB_TIMEOUT: Duration = Duration::from_secs(30);

/// A gated verb's `reason` check. Self-asserted, used for the log and the
/// user's Activity Log — never for authorization (the SDK's rule, not ours).
fn require_reason(args: &Value, verb: &str, why: &str) -> Option<Value> {
    if arg_str(args, "reason").is_some() {
        return None;
    }
    Some(err_code(
        "reason_required",
        format!("{verb} needs reason:\"why\" — {why}"),
        "The reason is written for the human who owns this desktop: it appears in their Activity \
         Log next to your AI thread name. Say what you are doing and why, in one line.",
    ))
}

fn require_confirm(args: &Value, verb: &str, what: &str) -> Option<Value> {
    if arg_bool(args, "confirm", false) {
        return None;
    }
    Some(err_code(
        "confirm_required",
        format!("{verb} would {what} — pass confirm:true to go ahead"),
        "This verb changes something the user cannot get back by rerunning a read verb, so it \
         asks once. Nothing has happened yet.",
    ))
}

/// One `app_state` call, giving everything the ownership decision needs: which
/// SOLIDWORKS session this is, what is open in it, and which document is active.
pub(crate) fn ownership_context(ctx: &Ctx) -> (Option<i64>, Vec<Value>, Option<String>, bool) {
    match ctx.call("app_state", json!({}), 30) {
        Ok(s) => (
            s.get("processId").and_then(|v| v.as_i64()),
            s.get("documents").and_then(|d| d.as_array()).cloned().unwrap_or_default(),
            s.get("activeDocument").and_then(|t| t.as_str()).map(String::from),
            // TRUNCATED: too many documents to enumerate, so `documents` is
            // empty as "unlistable", not as "none open". Ownership must not be
            // pruned against it.
            s.get("documentsTruncated").and_then(|v| v.as_bool()).unwrap_or(false),
        ),
        Err(_) => (None, Vec::new(), None, false),
    }
}

/// Prune ownership against the open set — but NEVER when the set is truncated,
/// because an empty "unlistable" set would disown every document the bridge
/// created (bug #5's shape, reached through the import-temp leak).
pub(crate) fn prune_unless_truncated(
    ctx: &Ctx,
    session: Option<i64>,
    open: &[Value],
    truncated: bool,
) {
    if truncated {
        return;
    }
    ctx.prune_scratch(session, &open_identities(open));
}

/// (title, path) for each open document — what pruning and ambiguity checks need.
pub(crate) fn open_identities(open: &[Value]) -> Vec<(String, Option<String>)> {
    open.iter()
        .map(|d| {
            (
                d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string(),
                d.get("path").and_then(|p| p.as_str()).filter(|s| !s.is_empty()).map(String::from),
            )
        })
        .collect()
}

/// Was that failure really "SOLIDWORKS has no room left"? Returns the honest
/// `host_app_exhausted` when it was, and `None` when there is no evidence for
/// it — an unreadable memory figure never manufactures the verdict.
pub(crate) fn exhausted_error(ctx: &Ctx, e: &crate::comhost::SidecarError) -> Option<Value> {
    // THE ONE SUFFICIENT SIGNAL. Not "this call failed and memory looks high":
    // the first version relabelled EVERY sidecar error as exhaustion whenever
    // the session was large, which on a healthy 13.1 GB machine turned a
    // template typo into "close your documents and restart SOLIDWORKS".
    let created_nothing = e.message.contains("created no document");
    if !created_nothing {
        return None;
    }
    let os = crate::launchos::os();
    let status = ctx.call("status", json!({}), 10);
    let open_docs = status.as_ref().ok().and_then(|r| r.get("openDocs").and_then(|v| v.as_u64()));
    let com_pid = status
        .as_ref()
        .ok()
        .and_then(|r| r.get("processId").and_then(|v| v.as_u64()))
        .map(|p| p as u32);
    let health = crate::launch::host_app_health(os.as_ref(), open_docs, Some(false), com_pid);
    let why = crate::launch::exhaustion(None, open_docs, created_nothing)?;
    let mut out = err_code(
        "host_app_exhausted",
        format!("SOLIDWORKS is running but out of room — {why}"),
        crate::launch::EXHAUSTED_HINT,
    );
    if let Value::Object(map) = &mut out {
        map.insert("hostAppError".into(), json!(e.message));
        map.insert("solidworksProcess".into(), health);
    }
    Some(out)
}

fn doc_title(d: &Value) -> String {
    d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string()
}

fn doc_path(d: &Value) -> Option<String> {
    d.get("path").and_then(|p| p.as_str()).filter(|s| !s.is_empty()).map(String::from)
}

/// Resolve WHICH DOCUMENT a verb is aimed at. Identity only — permission is
/// [`crate::writes::authorize`]'s job, and separating the two is what stopped
/// five rounds of "this route forgot to check".
///
/// Every document-addressing verb goes through here, read or write, because
/// acting on the wrong document is the harm either way: a BOM off the wrong
/// same-titled part is a bill of materials somebody will order from.
///
/// Order: explicit `targetDocument` (a TITLE), then `path`, then the active
/// document. A title matching more than one open document is `ambiguous_target`;
/// so is a PATH matching more than one, because `save_as` can put two open
/// documents at the same path. A `targetDocument` naming nothing open is an
/// error, never a silent fall back.
pub(crate) fn resolve_document(ctx: &Ctx, args: &Value, verb: &str) -> Result<Target, Value> {
    let (session, open, active, truncated) = ownership_context(ctx);
    prune_unless_truncated(ctx, session, &open, truncated);
    // A TRUNCATED SNAPSHOT CANNOT RESOLVE A DOCUMENT. `open` is empty because
    // the set is unlistable, so every lookup below would answer "no open
    // document is titled X" — a confident wrong answer about the user's session,
    // and the same empty-to-confident collapse this bridge has been rewritten
    // four times to stop making. Refuse with what is actually true.
    if truncated {
        return Err(err_code(
            "document_list_unavailable",
            format!(
                "{verb} cannot identify its target: SOLIDWORKS has more documents open than can \
                 be enumerated (or would not say how many), so the open-document list is UNKNOWN \
                 — not empty"
            ),
            "This is almost always leaked STEP/IGES import temporaries: run \
             swx_close_import_temps to drain them, then retry. swx_docs reports the count and a \
             sample. Nothing was changed, and this bridge still owns every document it created — \
             the ownership registry is deliberately NOT pruned against an unlistable session.",
        ));
    }
    resolve_target_inner(ctx, args, verb, session, &open, active.as_deref())
}

#[allow(clippy::too_many_arguments)]
fn resolve_target_inner(
    ctx: &Ctx,
    args: &Value,
    verb: &str,
    session: Option<i64>,
    open: &[Value],
    active: Option<&str>,
) -> Result<Target, Value> {
    // One pass over a list read ONCE. An earlier version called swx_docs from
    // inside the match closure — a COM round trip per open document, each
    // re-reading the ACTIVE document, which follows window focus. A click
    // mid-scan could match document i against an active document that was only
    // current at iteration i.
    let ambiguous = |title: &str| -> Value {
        let places: Vec<Value> = open
            .iter()
            .filter(|d| crate::verbs::same_title(&doc_title(d), title))
            .map(|d| json!({"title": doc_title(d), "path": doc_path(d)}))
            .collect();
        err_code(
            "ambiguous_target",
            format!("{verb}: {} open documents are titled '{title}'", places.len()),
            "A title is not an identity — two folders can each hold a file of that name, and              SOLIDWORKS will hand back whichever it finds first. Pass `path` with the full              filename of the one you mean. `candidates` lists them.",
        )
        .as_object()
        .map(|m| {
            let mut m = m.clone();
            m.insert("candidates".into(), Value::Array(places));
            Value::Object(m)
        })
        .unwrap_or(Value::Null)
    };

    let by_title = |t: &str| -> Result<Option<Value>, Value> {
        let hits: Vec<&Value> = open.iter().filter(|d| crate::verbs::same_title(&doc_title(d), t)).collect();
        match hits.len() {
            0 => Ok(None),
            1 => Ok(Some(hits[0].clone())),
            _ => Err(ambiguous(t)),
        }
    };
    // M1: `.find()` took the first match, so ambiguity was enforced on TITLE
    // only — and `save_as` can put two open documents at the same path, which
    // makes round 4's defect re-expressible with the very key that replaced it.
    let by_path = |p: &str| -> Result<Option<Value>, Value> {
        let hits: Vec<&Value> = open
            .iter()
            .filter(|d| doc_path(d).as_deref().map(|q| crate::verbs::same_path(q, p)).unwrap_or(false))
            .collect();
        match hits.len() {
            0 => Ok(None),
            1 => Ok(Some(hits[0].clone())),
            _ => Err(err_code(
                "ambiguous_target",
                format!("{verb}: {} open documents share the path '{p}'", hits.len()),
                "Two open documents report the same path, which SOLIDWORKS permits after a \
                 save_as. Close one, or name the document you mean with targetDocument.",
            )),
        }
    };

    let wanted = arg_str(args, "targetDocument");
    let path_arg = arg_str(args, "path");
    let doc = match (wanted, path_arg) {
        (Some(t), Some(p)) => {
            let a = by_title(t)?;
            let b = by_path(p)?;
            let same = a.as_ref().map(doc_title) == b.as_ref().map(doc_title)
                && a.as_ref().and_then(doc_path) == b.as_ref().and_then(doc_path);
            if !same {
                return Err(err_code(
                    "ambiguous_target",
                    format!("{verb} was given targetDocument:'{t}' AND path:'{p}', which name different documents"),
                    "Pass one or the other. Naming a document two ways and letting the bridge                      pick is exactly how a change lands somewhere the caller did not intend.",
                ));
            }
            a
        }
        (Some(t), None) => match by_title(t)? {
            Some(d) => Some(d),
            None => {
                return Err(err_code(
                    "document_not_open",
                    format!("{verb}: no open document is titled '{t}'"),
                    "swx_docs lists the open titles AND their paths. This is an ERROR rather                      than a fall back to the active document ON PURPOSE: silently retargeting a                      rename or a rebuild onto whatever the user happens to have in front of them                      is the failure this argument exists to prevent.",
                ));
            }
        },
        (None, Some(p)) => by_path(p)?,
        (None, None) => match active {
            Some(t) => by_title(t)?,
            None => None,
        },
    };
    let Some(doc) = doc else {
        return Err(err_code(
            "no_target_document",
            format!("{verb}: no document to act on"),
            "Open one (swx_open), create one (swx_new_document), or name one with              targetDocument (a title) or path (unambiguous). Nothing was changed.",
        ));
    };
    let title = doc_title(&doc);
    let path = doc_path(&doc);
    let owned = ctx.owns_doc(&title, path.as_deref(), session);
    Ok(Target { title, path, owned })
}

/// The COM-reported main window handle, or None. Cheap, and the only way to
/// attribute a desktop window to SOLIDWORKS with certainty.
fn main_hwnd(ctx: &Ctx) -> Option<i64> {
    window_identity(ctx).0
}

/// (main window handle, SOLIDWORKS process id) — the two facts that let a
/// desktop window be ATTRIBUTED rather than guessed at.
pub(crate) fn window_identity_for(ctx: &Ctx) -> (Option<i64>, Option<i64>) {
    window_identity(ctx)
}

fn window_identity(ctx: &Ctx) -> (Option<i64>, Option<i64>) {
    match ctx.call("app_state", json!({}), 20) {
        Ok(v) => (
            v.get("mainHwnd").and_then(|h| h.as_i64()),
            v.get("processId").and_then(|p| p.as_i64()),
        ),
        Err(_) => (None, None),
    }
}

// ── lifecycle & app state ───────────────────────────────────────────────────

/// `swx_get_app_state` — the everyday "what is SOLIDWORKS doing" probe, in the
/// `fusion_get_app_state` shape plus the facts an MCAD caller actually needs.
pub fn get_app_state(ctx: &Ctx, _args: &Value) -> Value {
    gate!(ctx);
    let detection = ctx.detection();
    match ctx.call("app_state", json!({}), 30) {
        Err(e) => sidecar_error(e),
        Ok(r) => ok_merge(
            r,
            json!({
                "solidworks": detection.to_json(),
                "windowEtiquette": "background",
                "_hint": "running/activeDocument/documents/workspace/busy in one call — the \
                          verb to poll while something long runs. `workspace` is the active \
                          document's type (part/assembly/drawing/none), the closest thing \
                          SOLIDWORKS has to Fusion's workspaces. `unsavedDocuments` > 0 means \
                          a close or a quit would raise a dialog nobody is there to answer.",
            }),
        ),
    }
}

/// `swx_document_info` — metadata for one document.
pub fn document_info(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let target = match resolve_document(ctx, args, "swx_document_info") {
        Ok(t) => t,
        Err(e) => return e,
    };
    match ctx.call("document_info", target.args(), 60) {
        Err(e) => sidecar_error(e),
        Ok(r) => ok_merge(
            r,
            json!({"_hint": "isSaved:false means the document has never been written to disk — \
                             SOLIDWORKS reports no path for those, and closing one without \
                             swx_save_as loses it. `configurations` is what swx_props_get and \
                             swx_bom can be scoped to."}),
        ),
    }
}

/// `swx_activate_document` — make an open document the active one.
pub fn activate_document(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let name = match arg_str(args, "name").or_else(|| arg_str(args, "path")) {
        Some(n) => n.to_string(),
        None => {
            return err(
                "name is required",
                "The document TITLE as swx_docs reports it (e.g. \"bracket.SLDPRT\"). Most \
                 analysis verbs take `path` instead and need no activation at all — activating \
                 changes what the user sees, targeting by path does not.",
            )
        }
    };
    match ctx.call("activate_document", json!({"name": name}), 110) {
        Err(e) => sidecar_error(e),
        Ok(r) => {
            crate::ledger::record("document_activated", json!({"name": r.get("activeDocument")}));
            ok_merge(
                r,
                json!({
                    "windowEtiquette": "background",
                    "_hint": "Activation changes which document the user is looking at. Prefer \
                              passing `path` to the analysis verbs when you only need to READ \
                              something — that touches nothing on screen.",
                }),
            )
        }
    }
}

/// THE RECOVERY FOLDER for one sweep: `<scratch>/recovery/<yyyymmdd-hhmmss>/`.
///
/// One folder per sweep, named by the clock, so a second sweep never lands in the
/// first one's directory and `swx_recover_list` can show the user what came from
/// when.
/// WHAT TO TELL THE USER ABOUT ONE FILE, given a verdict somebody actually made.
fn how_to_open(verified: bool, kind_known: bool, reason: &str, guessed_name: bool) -> String {
    if !verified {
        return format!(
            "NOT A RESCUE. The sweep declined to close against this file: {}. The document it \
             came from was left OPEN — if it is still open, its work is in SOLIDWORKS, not here. \
             These bytes are kept because they are the only ones there are, not because they are \
             a complete document.",
            if reason.is_empty() { "it could not be verified" } else { reason }
        );
    }
    if !kind_known {
        return format!(
            "SOLIDWORKS will not open this file under this name{}. RENAME it to .SLDPRT, .SLDASM \
             or .SLDDRW — the kind could not be established when it was written, so try them in \
             that order — then swx_open the renamed file. The bytes were verified as a document; \
             only the name is unknown.",
            if guessed_name {
                " — its extension was a GUESS made when the document's type could not be read, \
                 not something this bridge observed"
            } else {
                ""
            }
        );
    }
    "swx_open this path.".to_string()
}

/// A FILE WITH NO RECORD BESIDE IT.
///
/// Older sweeps wrote no manifest, and a manifest can be lost. What can still be
/// checked is checked — but the result is NOT the same answer the sweep would
/// have given, and must not be dressed as one:
///
///   * the bytes are not a document of any kind: that is CONCLUSIVE, and false.
///   * the bytes pass: that is NOT conclusive. This verb cannot know what the
///     original weighed, nor whether the extension was a guess, and both of those
///     are what the unknown-kind rules turn on. So the honest answer is UNKNOWN —
///     `null` — because a weaker check does not produce false negatives, it
///     produces FALSE POSITIVES, and `verified:true` for a file the sweep refused
///     is the one error that costs the user something.
fn reverified_entry(
    p: &std::path::Path,
    name: &str,
    bytes: Option<u64>,
    kind: Option<crate::disk::DocKind>,
    v: &crate::comhost::host::RecoveryVerdict,
) -> Value {
    let conclusive_no = !v.ok;
    json!({
        "path": p.to_string_lossy(),
        "name": name,
        "bytes": bytes,
        "source": "reverified",
        "kind": kind.map(|k| json!(k.type_name())).unwrap_or(Value::Null),
        "kindKnown": kind.is_some(),
        "signatureOk": v.signature_ok,
        "verified": if conclusive_no { json!(false) } else { Value::Null },
        "verifiedReason": if conclusive_no {
            v.why.clone().map(Value::from).unwrap_or(Value::Null)
        } else {
            json!("re-checked from the file alone: this sweep left no manifest, so this bridge \
                   cannot see what the original document weighed or whether the extension was a \
                   guess — both of which the sweep's own verdict turned on. The bytes look like a \
                   document; whether the sweep accepted them is UNKNOWN.")
        },
        "howToOpen": if conclusive_no {
            format!(
                "NOT A RESCUE. These bytes are not a complete document: {}. If the document is \
                 still open in SOLIDWORKS, its work is there rather than here.",
                v.why.clone().unwrap_or_else(|| "it could not be verified".into())
            )
        } else {
            "UNCONFIRMED. No manifest was left for this sweep, so this bridge cannot say whether \
             it vouched for this file. Try swx_open (renaming a `.sldrecover` file to .SLDPRT, \
             .SLDASM or .SLDDRW first); if SOLIDWORKS refuses it, the file is incomplete and the \
             document's work is wherever it was left."
                .to_string()
        },
    })
}

/// A HOST FAILURE, WITH ITS STRUCTURE INTACT.
///
/// The sweep used to build `{"why": "the com host errored: <message>"}` at five
/// call sites and drop `e.data` at every one — so the structured COM detail 0.7.8
/// exists to surface could not reach a caller from this verb no matter what the
/// host sent. That matters more here than anywhere else: this is the verb that
/// runs when SOLIDWORKS is already misbehaving, so it meets the HRESULTs worth
/// reading, and since 0.7.8 its entries are written into `manifest.json` and
/// KEPT — a prose-only verdict is the permanent record of why a document was not
/// preserved.
fn host_failure_entry(e: &crate::comhost::SidecarError, path: Option<&str>) -> Value {
    json!({
        "path": path,
        "why": format!("the com host errored: {}", e.message),
        "detail": e.detail,
        "errorCode": e.code,
        "timedOut": e.timed_out,
        // THE STRUCTURED REASON. Where the host answered with one, it is carried
        // verbatim. Where it did NOT answer at all — a timeout, which kills the
        // com host — there is no host detail to carry, and `null` there is
        // indistinguishable from having thrown the detail away. So a timeout
        // reports ITS OWN facts instead: what this was, that the budget was
        // exceeded, and that the host was killed and respawns. Both are real
        // structure; neither is invented.
        "hostError": e.data.clone().unwrap_or_else(|| {
            if e.timed_out {
                json!({
                    "failure": "host_timeout",
                    "timedOut": true,
                    "message": e.message,
                    "note": "the com host did not answer within its budget and was killed; it \
                             respawns on the next verb. No COM detail exists for this failure \
                             because SOLIDWORKS never returned one.",
                })
            } else {
                Value::Null
            }
        }),
    })
}

/// THE NAME OF THE SWEEP'S OWN RECORD, written beside the files it wrote.
pub const MANIFEST_NAME: &str = "manifest.json";

/// WHAT THE SWEEP DECIDED, WRITTEN DOWN WHERE IT DECIDED IT.
///
/// Everything `swx_recover_list` used to re-derive was in hand at save time and
/// thrown away when the process ended: the original document's size, the kind
/// `GetType` reported, and whether the file's extension was a GUESS
/// (`namingFallback`). Re-deriving a verdict without them is strictly weaker than
/// the verdict it is trying to reproduce — and a weaker check does not produce
/// false negatives, it produces FALSE POSITIVES. `verified:true` for a file the
/// sweep refused and whose document it left open is the one error that matters
/// here, because the person reading the listing is the person whose sweep just
/// failed.
///
/// So the sweep writes its record into its own folder. It is rewritten after
/// every save and every close, ATOMICALLY (a temporary file and a rename), so a
/// bridge that dies mid-sweep leaves a manifest describing everything decided up
/// to that moment rather than a truncated one — and that is the case that matters
/// most, because a sweep that ran during a crash is exactly the one whose files
/// somebody reads later from a fresh bridge.
fn write_manifest(
    dir: &std::path::Path,
    reason: &str,
    records: &[Value],
    closed: &[String],
    failures: &[Value],
) {
    let files: Vec<Value> = records
        .iter()
        .map(|r| {
            let rp = r.get("recoveryPath").and_then(|v| v.as_str()).unwrap_or("");
            let name = std::path::Path::new(rp)
                .file_name()
                .map(|n| n.to_string_lossy().to_string())
                .unwrap_or_default();
            // A DOCUMENT CLOSED IN PLACE. A clean, saved document needs no copy —
            // its own file IS its preservation — so the sweep writes nothing for
            // it and `recoveryPath` is null. Without saying so here, the record
            // had no entry a reader could find, and swx_recover_list could not
            // tell a user "your file was closed at its own path, untouched",
            // which is the most reassuring thing it has to say. Measured live
            // 2026-09-30: Drew's saved part was closed with its bytes and mtime
            // unchanged, and the listing had nothing to show for it.
            let original = r.get("path").and_then(|v| v.as_str()).unwrap_or("");
            let in_place = rp.is_empty()
                && r.get("preserved").and_then(|v| v.as_bool()) == Some(true)
                && !original.is_empty();
            let close_key = if in_place { original } else { rp };
            json!({
                "name": if in_place { Value::Null } else { json!(name) },
                "closedInPlace": in_place,
                "recoveryPath": r.get("recoveryPath").cloned().unwrap_or(Value::Null),
                "title": r.get("title").cloned().unwrap_or(Value::Null),
                "originalPath": r.get("path").cloned().unwrap_or(Value::Null),
                // The kind SOLIDWORKS reported, or null when nothing could
                // establish it. A reader must not infer this from the extension.
                "kind": r.get("docKind").cloned().unwrap_or(Value::Null),
                // THE FACT NO FILE NAME CAN CARRY: this extension was a guess.
                "namingFallback": r.get("namingFallback").cloned().unwrap_or(Value::Null),
                "originalBytes": r.get("originalBytes").cloned().unwrap_or(Value::Null),
                // The user's own file's modification time when the sweep reached
                // it — recorded for a document closed in place, so a later reader
                // can tell "untouched since" from "written to since".
                "originalModifiedMs": r.get("originalModifiedMs").cloned().unwrap_or(Value::Null),
                "recoveryBytes": r.get("recoveryBytes").cloned().unwrap_or(Value::Null),
                "signatureOk": r.get("recoverySignatureOk").cloned().unwrap_or(Value::Null),
                // THE SWEEP'S VERDICT AND ITS OWN WORDS FOR IT. `verified` is a
                // verdict about bytes THIS SWEEP WROTE, so a document closed in
                // place has none: null, not true — nothing was written to verify.
                "verified": if in_place {
                    Value::Null
                } else {
                    r.get("preserved").cloned().unwrap_or(Value::Null)
                },
                "verdictReason": match r.get("why").filter(|v| !v.is_null()) {
                    Some(w) => w.clone(),
                    None if in_place => json!(
                        "clean and saved when the sweep reached it: its own file is its \
                         preservation, so the sweep wrote no copy and closed it at its own path \
                         without writing to that file"
                    ),
                    None => Value::Null,
                },
                "preserved": r.get("preserved").cloned().unwrap_or(Value::Null),
                // CLOSED BY THE PATH IT WAS CLOSED BY — its own, for a document
                // closed in place.
                "closed": !close_key.is_empty()
                    && closed.iter().any(|c| crate::verbs::same_path(c, close_key)),
                "wasDirty": r.get("wasDirty").cloned().unwrap_or(Value::Null),
                "wasPathless": r.get("wasPathless").cloned().unwrap_or(Value::Null),
            })
        })
        .collect();
    let doc = json!({
        "manifestVersion": 1,
        // WHY THE SWEEP STOPPED SHORT, with whatever structure the host gave.
        // A host failure explains documents that have NO file here at all, which
        // is exactly the question somebody reading this folder later is asking —
        // and it was the one thing the record did not answer.
        "failures": failures,
        "sweep": dir.file_name().map(|n| n.to_string_lossy().to_string()),
        "reason": reason,
        "updatedAtMs": crate::launchos::epoch_ms() as u64,
        "bridgeVersion": crate::BRIDGE_VERSION,
        "files": files,
        "_note": "Written by swx_close_all_safely / swx_shutdown as it ran. `verified:false` means \
                  this bridge REFUSED to close the document against these bytes and left the \
                  document open — the work is in SOLIDWORKS, not in this file. Kept because these \
                  are the only bytes there are. `closedInPlace:true` means the document was clean \
                  and saved: no copy was written (`name` and `recoveryPath` are null), and it was \
                  closed at `originalPath`, a file this sweep never writes to.",
    });
    let Ok(text) = serde_json::to_string_pretty(&doc) else { return };
    // ATOMIC: a half-written manifest would be worse than none, because the
    // listing believes it.
    let tmp = dir.join("manifest.json.tmp");
    if std::fs::write(&tmp, text.as_bytes()).is_ok() {
        let _ = std::fs::rename(&tmp, dir.join(MANIFEST_NAME));
    }
}

fn recovery_dir() -> Result<std::path::PathBuf, String> {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    // yyyymmdd-hhmmss without pulling in a date library: the epoch arithmetic is
    // exact and this only has to be unique and sortable.
    let days = now / 86_400;
    let secs = now % 86_400;
    let (mut y, mut d) = (1970u64, days);
    loop {
        let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
        let len = if leap { 366 } else { 365 };
        if d < len {
            break;
        }
        d -= len;
        y += 1;
    }
    let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
    let months = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    let (mut m, mut day) = (0usize, d);
    while m < 12 && day >= months[m] {
        day -= months[m];
        m += 1;
    }
    let stamp = format!(
        "{y:04}{:02}{:02}-{:02}{:02}{:02}",
        m + 1,
        day + 1,
        secs / 3600,
        (secs % 3600) / 60,
        secs % 60
    );
    // ONE FOLDER PER SWEEP, even two sweeps in the same second.
    //
    // The stamp is second-granular, so two sweeps a fraction apart used to SHARE
    // a folder — and since 0.7.8 that means sharing a `manifest.json`. The second
    // sweep's manifest overwrites the first's, so the first sweep's files are
    // still there with no record describing them; and if the second preserved
    // nothing, its cleanup DELETED the first's manifest outright. That is the
    // mechanism behind the intermittent
    // `a_sweep_records_its_verdicts_beside_the_files_it_wrote` failure — roughly
    // one run in twelve — and it is a real reporting defect, not a test artefact:
    // the record of why a user's document was not preserved is what goes missing.
    // CLAIMED ATOMICALLY, not check-then-create. `if !exists { create }` is a
    // race with a window between the two halves: two sweeps starting together
    // both see "free" and both take the same folder — which is the defect this
    // uniqueness was added to fix, reintroduced by the way it was implemented.
    // `create_dir` FAILS if the directory is already there, so the creation IS
    // the claim, and AlreadyExists simply means take the next name.
    // A FOLDER THAT CANNOT BE MADE IS AN ERROR, NOT A PATH (adversary pass 40).
    // This used to drop `create_dir_all`'s error and hand back the path anyway, so
    // a sweep on a blocked recovery location — no permission, a quota, a FILE in
    // the way — made one doomed SaveAs3 per document, on a SOLIDWORKS that may
    // already be struggling, and reported N save failures instead of one cause.
    let base = crate::scratch::root().join("recovery");
    std::fs::create_dir_all(&base).map_err(|e| {
        format!("the recovery folder {} cannot be created: {e}", base.display())
    })?;
    for n in 0..1000 {
        let d = if n == 0 { base.join(&stamp) } else { base.join(format!("{stamp}-{}", n + 1)) };
        match std::fs::create_dir(&d) {
            Ok(()) => return Ok(d),
            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
            // Anything else — no permission, no disk — is not a naming problem.
            Err(e) => {
                return Err(format!("this sweep's folder {} cannot be created: {e}", d.display()))
            }
        }
    }
    let d = base.join(format!("{stamp}-{}", crate::launchos::epoch_ms()));
    std::fs::create_dir(&d)
        .map_err(|e| format!("this sweep's folder {} cannot be created: {e}", d.display()))?;
    Ok(d)
}

/// IS A DIALOG ON SCREEN? The sweep must never reach a save prompt — every
/// document is clean before anything closes — so one appearing means a premise
/// is wrong, and the answer is to STOP and report it, never to answer it.
///
/// `None` means the census could not be taken, which also stops: a dialog we
/// cannot see is the one that matters.
fn blocking_dialog_now(ctx: &Ctx) -> Option<Option<Value>> {
    dialog_census_now(ctx).map(|c| c.blocking.map(|d| d.to_json()))
}

/// The NOTIFICATIONS on screen now — windows the census judged not blocking —
/// with their evidence, for the reply. Empty when the census cannot be taken.
fn notifications_now(ctx: &Ctx) -> Vec<Value> {
    dialog_census_now(ctx)
        .map(|c| c.notifications.iter().map(|d| d.to_json()).collect())
        .unwrap_or_default()
}

fn dialog_census_now(ctx: &Ctx) -> Option<crate::quit::DialogCensus> {
    let os = crate::launchos::os();
    let state = ctx.call("app_state", json!({}), 20);
    let pid = state
        .as_ref()
        .ok()
        .and_then(|s| s.get("processId"))
        .and_then(|v| v.as_u64())
        .map(|p| p as u32)?;
    let census = os.processes().ok()?;
    let created_at = census.iter().find(|p| p.pid == pid).and_then(|p| p.created_at)?;
    dialog_census_of(os.as_ref(), &census, pid, created_at)
}

/// The judged census for one SOLIDWORKS process — no COM involved: the windows
/// of the process and its children, each judged by `quit::judge_window`.
fn dialog_census_of(
    os: &dyn crate::launchos::LaunchOs,
    census: &Vec<crate::launchos::ProcInfo>,
    pid: u32,
    created_at: u64,
) -> Option<crate::quit::DialogCensus> {
    let id = crate::quit::Identity { pid, created_at, main_hwnd: None };
    let pids = crate::quit::dialog_census_pids(Ok(census), &id)?;
    let mut windows: Vec<crate::launchos::WindowInfo> = Vec::new();
    for p in &pids {
        match os.windows_of_pid(*p) {
            Ok(w) => windows.extend(w),
            Err(_) => return None,
        }
    }
    // AND READ WHAT IT SAYS. The sweep stops on a dialog and reports it; a title
    // alone cannot tell "your licence expired" from "the licence server is
    // unreachable and the session ends after five more tries". Reading is not
    // answering — nothing here presses anything.
    // JUDGED BY MEASURED MODALITY (0.9.4): a notification — owner frame enabled,
    // a tool window, nothing to click — does not stop a sweep or an import.
    Some(crate::quit::dialog_census(&windows, os))
}

/// `swx_close_all_safely` — PRESERVE EVERYTHING, THEN CLOSE EVERYTHING.
///
/// Drew, 2026-09-11: *"I despise every time you ask for help closing. You MUST
/// find/build a way to safely close the docs/program."*
///
/// So this verb does not ask. It does not check ownership either — the user
/// authorized closing their own documents, and whose document it is stops being
/// the question. What replaces the question is a SEQUENCE, and it is the whole
/// safety property of this verb:
///
///   **every document is written to disk and verified there BEFORE anything
///   closes, and a document that could not be written is not closed.**
///
/// A dirty document is SAVED TO RECOVERY — `SaveAs3`, Silent alone, never Copy —
/// so the open document then POINTS AT the recovery file. That re-point is not a
/// detail: it is what makes the close key right, because the close is by the path
/// the document has at that moment. The user's original file on disk is never
/// written to; it is simply no longer what this in-memory document is attached
/// to, and their unsaved changes now live in recovery.
///
/// A save prompt cannot appear, because nothing is unsaved by the time a close
/// happens. If any dialog DOES appear the sweep stops and reports it rather than
/// answering it — and if the window census cannot be READ, the saves still happen
/// (they lose nothing) while the closes do not (they cannot be taken back).
/// The longest a sweep or drain runs before it answers `done:false` — always
/// inside the budget `bridge.json` declares to ab for these verbs (600 s), so
/// the reply arrives before ab gives up on it (MEASURED 2026-10-02: undeclared,
/// ab cut a sweep's reply at its 120 s default while the sweep ran on).
pub const SWEEP_MAX_SECS: u64 = 540;

pub fn close_all_safely(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    if let Some(g) = require_reason(
        args,
        "swx_close_all_safely",
        "it closes every open SOLIDWORKS document after saving each one to a recovery folder",
    ) {
        return g;
    }
    if let Err(e) = identity_claim_ok(ctx) {
        return e;
    }
    // NOT WHERE THE OS CLEANS. With no user profile location at all the data root
    // falls back to the temp directory, which Storage Sense and Disk Cleanup empty
    // on a schedule BY DESIGN — the one standard place the only copy of unsaved
    // work must never go. Refused before anything is written or closed.
    if crate::scratch::root_is_temp_fallback() {
        return err_code(
            "data_root_is_temp",
            format!(
                "refusing to write recovery files to {}: neither LOCALAPPDATA nor USERPROFILE is \
                 set, so the bridge's data root fell back to the OS temp directory, which Windows \
                 empties on a schedule. A recovery file is the user's only copy of unsaved work.",
                crate::scratch::root().display()
            ),
            "Nothing was preserved and nothing was closed. Start the bridge with LOCALAPPDATA \
             (or USERPROFILE) set — it is set in every normal Windows user session — or point \
             SWX_SCRATCH_DIR at a directory nothing cleans, and run this again.",
        );
    }
    // NOT UNDER AN INSTALL LOCATION. A recovery folder is the user's only copy
    // of unsaved work. The install directory is re-extracted on update (measured
    // 2026-09-30: the 2026-09-18 auto-install of 0.8.9 deleted every recovery
    // folder from the day before), and ab's own directory belongs to ab's
    // installer and uninstaller (adversary pass 39). The data root is outside both
    // by construction; this is the check that refuses, loudly and before anything
    // is written or closed, if an override or a future change ever puts it back.
    if let Err(why) = crate::scratch::refuse_inside_install(&crate::scratch::root().join("recovery")) {
        return err_code(
            "recovery_inside_install_dir",
            why,
            "Nothing was preserved and nothing was closed. Point the bridge's data root at a \
             directory only it writes to — the default is %LOCALAPPDATA%\\adom-solidworks-bridge \
             — and run this again.",
        );
    }
    // A RECOVERY FOLDER THAT CANNOT BE MADE IS REFUSED ONCE, HERE — before the
    // first SaveAs, with the one cause, instead of being discovered one doomed
    // save at a time.
    let dir = match recovery_dir() {
        Ok(d) => d,
        Err(why) => {
            return err_code(
                "recovery_dir_unwritable",
                why,
                "Nothing was preserved and nothing was closed. The bridge could not create its \
                 recovery folder; the reason is above (permissions, a quota, a file where the \
                 folder must go). Fix that — or point SWX_SCRATCH_DIR somewhere writable — and run \
                 this again.",
            )
        }
    };
    let job = ctx.jobs.start("close_all_safely", "Saving every document, then closing it", 120);
    let dir_s = dir.to_string_lossy().to_string();
    let started = std::time::Instant::now();
    let budget = std::time::Duration::from_secs(
        args.get("maxSeconds").and_then(|v| v.as_u64()).unwrap_or(240).clamp(5, SWEEP_MAX_SECS),
    );
    let batch = args.get("batch").and_then(|v| v.as_u64()).unwrap_or(25).clamp(1, 200);

    // ── HOW MUCH ROOM IS THERE, AND HOW MUCH DOES THIS NEED?
    //
    //    Marker and floor cannot catch the one failure that matters most here:
    //    THE RIGHT FILE, CUT SHORT. The marker lives at offset 4, so any prefix
    //    of a real document carries it, and a write that reaches 20 KB of a
    //    500 KB assembly passes the floor too. The format offers nothing cheap
    //    that would catch it — no constant tail, no length field (measured). So
    //    the defence is at the START: a truncation this bridge could not detect
    //    afterwards is one it declines to cause.
    //    THE KIND OF EACH DOCUMENT COMES FROM SOLIDWORKS, not from a guess: an
    //    assembly is about thirty times the median part, and costing a pathless
    //    one as if it were a part is how the per-save gate passes with far less
    //    room than the write needs.
    let open_docs: Vec<(String, Option<crate::disk::DocKind>)> =
        match ctx.call("docs", json!({}), 60) {
            Ok(d) => d
                .get("docs")
                .and_then(|v| v.as_array())
                .map(|a| {
                    a.iter()
                        .map(|x| {
                            let path =
                                x.get("path").and_then(|p| p.as_str()).unwrap_or("").to_string();
                            // The real backend reports `type`, the simulator
                            // `docType`; a TITLE is the last resort, and it is
                            // read as a title — its extension, or the prefix
                            // SOLIDWORKS gives an unsaved document ("Assem1").
                            // 0.7.4 ran the title through a substring match that
                            // called `Assem1` a part and costed it at 256 KB,
                            // which is worse than the 1 MB it would have cost
                            // with no hint at all. A document whose kind cannot
                            // be established stays None and costs the generic
                            // unknown.
                            let kind = x
                                .get("type")
                                .and_then(|v| v.as_str())
                                .and_then(|t| crate::disk::DocKind::from_type_name(t))
                                .or_else(|| {
                                    x.get("docType")
                                        .and_then(|v| v.as_str())
                                        .and_then(|t| crate::disk::DocKind::from_type_name(t))
                                })
                                .or_else(|| {
                                    x.get("title")
                                        .and_then(|v| v.as_str())
                                        .and_then(|t| crate::disk::DocKind::from_title(t))
                                });
                            (path, kind)
                        })
                        .collect()
                })
                .unwrap_or_default(),
            // An unreadable session is not an empty one. Estimate nothing from it
            // and let the per-save check carry the weight.
            Err(_) => Vec::new(),
        };
    let open_paths: Vec<String> = open_docs.iter().map(|(p, _)| p.clone()).collect();
    let estimate = crate::disk::estimate_writes_typed(&open_docs);
    let needed = estimate.total.saturating_mul(crate::disk::HEADROOM_FACTOR);
    let per_save_needed = estimate.largest.saturating_mul(crate::disk::HEADROOM_FACTOR);
    let free_before = crate::disk::free_bytes(&dir);
    let mut stopped_by_headroom: Option<Value> = None;
    // UNKNOWN FREE SPACE IS NOT PERMISSION, but it is not a refusal either: this
    // sweep's other guarantees do not depend on it, and refusing every sweep on a
    // volume this process cannot stat would put the bridge back to asking the
    // user to close things by hand. It is reported, and it is why
    // `headroomBytesBefore` can be null.
    if let Some(free) = free_before {
        if free < needed {
            stopped_by_headroom = Some(json!({
                "freeBytes": free,
                "neededBytes": needed,
                "shortfallBytes": needed.saturating_sub(free),
                "when": "before anything was written",
            }));
        }
    }

    let mut recovered: Vec<Value> = Vec::new();
    let mut closed_paths: Vec<String> = Vec::new();
    let mut close_failed: Vec<Value> = Vec::new();
    let mut closed = 0u64;
    let mut before: Option<i64> = None;
    let mut after: Option<i64> = None;
    let mut done = false;
    let mut stopped_by_dialog: Option<Value> = None;
    let mut dialog_unreadable = false;
    // THE CENSUS, ASKED AGAIN. Called before EVERY save and before EVERY close,
    // not once per batch: a save can raise a prompt of its own (a missing
    // external reference, a rebuild question), and under the old once-per-batch
    // check a dialog raised by the first save stayed invisible until every other
    // document in the batch had already been closed.
    //
    //   * `Some(Some(d))` — a dialog. STOP; it is never answered.
    //   * `None` — nobody can see. The SAVES still run, because saving loses
    //     nothing and is the half that protects the user's work; the CLOSES do
    //     not, because a dialog nobody can see is exactly the one that matters.
    //     Same rule as swx_stop's P3.
    //   * `Some(None)` — clear.
    enum Look {
        Clear,
        Blind,
        Dialog(Value),
    }
    let look = |ctx: &Ctx| -> Look {
        match blocking_dialog_now(ctx) {
            None => Look::Blind,
            Some(Some(d)) => Look::Dialog(d),
            Some(None) => Look::Clear,
        }
    };
    let mut preserved_keys: std::collections::HashSet<String> = std::collections::HashSet::new();
    let mut deferred_now: Vec<String> = Vec::new();
    let mut closed_by_parent: Vec<String> = Vec::new();
    'sweep: while stopped_by_headroom.is_none() {
        // ── PHASE 1: PRESERVE, one document at a time, censusing before each.
        //    Nothing closes during this phase, which is what makes the walk index
        //    a stable way to step through the session.
        // (path, kind, was-written-by-this-sweep, the original's size)
        let mut ready: Vec<(String, Option<crate::disk::DocKind>, bool, Option<u64>)> = Vec::new();
        let mut walked = 0u64;
        let mut index = 0u64;
        // THE WHOLE SESSION IS PRESERVED BEFORE ANYTHING CLOSES (0.9.12). The
        // close order must be GLOBAL: closing an assembly unloads the components
        // it holds, so every one of them must be on disk first, and a walk that
        // stopped after `batch` documents never reached the assembly at all —
        // MEASURED 2026-10-02: 28 parts first in enumeration order, then the
        // top-level assembly and four sub-assemblies; every close of a part was
        // "still open after CloseDoc", and the sweep stopped with 33 documents
        // open. `batch` now paces the CLOSES of a pass, not the preservation.
        loop {
            match look(ctx) {
                Look::Dialog(d) => {
                    stopped_by_dialog = Some(d);
                    break 'sweep;
                }
                Look::Blind => dialog_unreadable = true,
                Look::Clear => {}
            }
            if started.elapsed() >= budget {
                break;
            }
            // AGAIN, BEFORE EVERY SAVE. The disk can fill while this runs — this
            // sweep is itself one of the things filling it — and the check that
            // matters is the one taken before the write that would be truncated,
            // not the one taken before the first of them.
            if let Some(free) = crate::disk::free_bytes(&dir) {
                if free < per_save_needed {
                    stopped_by_headroom = Some(json!({
                        "freeBytes": free,
                        "neededBytes": per_save_needed,
                        "shortfallBytes": per_save_needed.saturating_sub(free),
                        "when": format!("after preserving {} document(s)", recovered.len()),
                    }));
                    break 'sweep;
                }
            }
            job.update(
                format!("Preserving document {} of {}", index + 1, before.map(|b| b.to_string()).unwrap_or_else(|| "?".into())),
                json!({"stage": "preserve", "index": index, "preserved": recovered.len(), "closed": closed,
                       "documentCountBefore": before, "elapsedSec": started.elapsed().as_secs()}),
            );
            let r = match ctx.call(
                "preserve_and_close",
                json!({"phase": "preserve", "recoveryDir": dir_s, "index": index}),
                300,
            ) {
                Err(e) => {
                    close_failed.push(host_failure_entry(&e, None));
                    // RECORD IT. A sweep that stops because the HOST failed is
                    // exactly the case where the folder holds fewer files than
                    // documents, and the manifest is the only thing that can say
                    // why.
                    write_manifest(
                        &dir,
                        arg_str(args, "reason").unwrap_or_default(),
                        &recovered,
                        &closed_paths,
                        &close_failed,
                    );
                    break 'sweep;
                }
                Ok(r) => r,
            };
            if before.is_none() {
                before = r.get("documentCount").and_then(|v| v.as_i64());
            }
            after = r.get("documentCount").and_then(|v| v.as_i64());
            if r.get("walked").and_then(|v| v.as_u64()).unwrap_or(0) == 0 {
                break; // the walk is exhausted
            }
            walked += 1;
            // A DOCUMENT WALKED AGAIN on a later pass (deferred behind its
            // assembly) was preserved already: its record is not repeated.
            let ready_path = r
                .pointer("/ready/path")
                .and_then(|v| v.as_str())
                .map(|p| p.replace('/', "\\").to_lowercase());
            let seen_before = ready_path.as_ref().map(|p| preserved_keys.contains(p)).unwrap_or(false);
            if let Some(p) = &ready_path {
                preserved_keys.insert(p.clone());
            }
            if let Some(rec) = r.get("record").filter(|v| !v.is_null() && !seen_before) {
                recovered.push(rec.clone());
                // AFTER EVERY SAVE. A sweep that dies here leaves a record of
                // everything decided up to this document.
                write_manifest(&dir, arg_str(args, "reason").unwrap_or_default(), &recovered, &closed_paths, &close_failed);
            }
            if let Some(rd) = r.get("ready").filter(|v| !v.is_null()) {
                ready.push((
                    rd.get("path").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
                    // The KIND the backend established, or None when it could
                    // not. It is carried, never re-derived from the file name —
                    // that name came FROM the kind.
                    rd.get("docType")
                        .and_then(|v| v.as_str())
                        .and_then(|t| crate::disk::DocKind::from_type_name(t)),
                    rd.get("writtenHere").and_then(|v| v.as_bool()).unwrap_or(false),
                    rd.get("originalBytes").and_then(|v| v.as_u64()),
                ));
            }
            index += 1;
        }

        // ── PHASE 2: CLOSE. Drawings and assemblies before parts — a part an
        //    assembly still references stays open when closed on its own, and
        //    closing the parent first lets the child go. Skipped entirely while
        //    the census is unreadable: everything is already safe on disk.
        //    A document of UNKNOWN kind is closed LAST: it might be a part held
        //    open by an assembly, and going late costs nothing.
        // GLOBAL ORDER: drawings, then assemblies — the ACTIVE document first,
        // it is usually the top level — then parts, then unknown kinds.
        let active = ownership_context(ctx).2.map(|t| t.to_lowercase());
        let is_active = |p: &str| {
            let base = p.rsplit(['\\', '/']).next().unwrap_or(p).to_lowercase();
            active.as_deref().map(|a| a == base || base.starts_with(&format!("{a}."))).unwrap_or(false)
        };
        ready.sort_by_key(|(p, k, _, _)| {
            let kind_rank = match k {
                Some(crate::disk::DocKind::Drawing) => 0,
                Some(crate::disk::DocKind::Assembly) => 1,
                Some(crate::disk::DocKind::Part) => 2,
                None => 3,
            };
            (kind_rank, if is_active(p) { 0 } else { 1 })
        });
        let mut closed_this_pass = 0u64;
        let mut attempts_this_pass = 0u64;
        deferred_now.clear();
        // Assemblies preserved in this pass and not (yet) closed: a component
        // or sub-assembly still open after CloseDoc while one of these is open
        // is DEFERRED behind it — neither a failure nor "no progress".
        let mut open_assemblies: Vec<String> = ready
            .iter()
            .filter(|(_, k, _, _)| *k == Some(crate::disk::DocKind::Assembly))
            .map(|(p, _, _, _)| p.clone())
            .collect();
        if !dialog_unreadable {
            for (path, kind, written_here, original_bytes) in &ready {
                if attempts_this_pass >= batch {
                    break;
                }
                match look(ctx) {
                    Look::Dialog(d) => {
                        stopped_by_dialog = Some(d);
                        break 'sweep;
                    }
                    // BLIND MID-SWEEP. Whatever was closed is closed and was on
                    // disk first; nothing further closes.
                    Look::Blind => {
                        dialog_unreadable = true;
                        break;
                    }
                    Look::Clear => {}
                }
                job.update(
                    format!("Closing {}", path.rsplit(['\\', '/']).next().unwrap_or(path)),
                    json!({"stage": "close", "preserved": recovered.len(), "closed": closed,
                           "deferred": deferred_now.len(), "documentCountBefore": before,
                           "documentCountNow": after, "elapsedSec": started.elapsed().as_secs()}),
                );
                match ctx.call(
                    "preserve_and_close",
                    json!({
                        "phase": "close", "path": path, "writtenHere": written_here,
                        "docKind": kind.map(|k| json!(k.type_name())).unwrap_or(Value::Null),
                        "originalBytes": original_bytes,
                    }),
                    300,
                ) {
                    Err(e) => {
                        close_failed.push(host_failure_entry(&e, Some(path)));
                        break 'sweep;
                    }
                    Ok(r) => {
                        after = r.get("documentCount").and_then(|v| v.as_i64());
                        if r.get("closed").and_then(|v| v.as_bool()).unwrap_or(false) {
                            closed += 1;
                            closed_this_pass += 1;
                            open_assemblies.retain(|a| a != path);
                            closed_paths.push(path.clone());
                            if r.get("alreadyClosed").and_then(|v| v.as_bool()) == Some(true) {
                                // GONE WITH ITS PARENT — after its own recovery
                                // write. Free: it does not count against `batch`.
                                closed_by_parent.push(path.clone());
                            } else {
                                attempts_this_pass += 1;
                            }
                            // AND AFTER EVERY CLOSE, so the record says which
                            // documents are gone from SOLIDWORKS and now exist
                            // only as these files.
                            write_manifest(
                                &dir,
                                arg_str(args, "reason").unwrap_or_default(),
                                &recovered,
                                &closed_paths,
                                &close_failed,
                            );
                        } else {
                            let why = r.get("why").and_then(|v| v.as_str()).unwrap_or("still open after CloseDoc");
                            let held = why.contains("still open after CloseDoc")
                                && *kind != Some(crate::disk::DocKind::Drawing)
                                && open_assemblies.iter().any(|a| a != path);
                            if held {
                                deferred_now.push(path.clone());
                            } else {
                                attempts_this_pass += 1;
                                close_failed.push(json!({"path": path, "why": why}));
                            }
                        }
                    }
                }
            }
        }
        // AN EMPTY SESSION after this pass: every preserved document not yet
        // accounted for went out with its parent (after its own recovery write).
        if after == Some(0) && !dialog_unreadable {
            for (path, _, _, _) in &ready {
                if !closed_paths.contains(path) {
                    closed += 1;
                    closed_this_pass += 1;
                    closed_paths.push(path.clone());
                    closed_by_parent.push(path.clone());
                }
            }
            deferred_now.clear();
            write_manifest(&dir, arg_str(args, "reason").unwrap_or_default(), &recovered, &closed_paths, &close_failed);
        }
        // DONE means the session is EMPTY and we can prove it — not that this
        // pass finished.
        done = !dialog_unreadable && after == Some(0) && close_failed.is_empty();
        // NOTHING CLOSED IN A PASS THAT TRIED EVERY READY DOCUMENT is no
        // progress, deferrals included: the next pass would ask the same
        // questions in the same order. Anything still deferred then is a failure.
        // A preserve-only pass closes nothing by design, so it runs ONCE.
        // NO PROGRESS IS A STOP, NOT A LOOP: a pass that walked documents and
        // closed none will close none next time either — the commonest cause is
        // that they could not be saved, which is exactly when this verb must
        // leave them alone and say so.
        if done || walked == 0 || dialog_unreadable || closed_this_pass == 0
            || started.elapsed() >= budget
        {
            break;
        }
    }
    // A DOCUMENT STILL DEFERRED WHEN THE SWEEP ENDS was not closed, and says why.
    for p in deferred_now.drain(..) {
        if !closed_paths.contains(&p) {
            close_failed.push(json!({"path": p, "why": "still open after CloseDoc: deferred behind an assembly that is still open"}));
        }
    }
    if !close_failed.is_empty() {
        done = false;
    }
    // AN EMPTY RESCUE FOLDER IS NOT A RESCUE. The folder is created before the
    // sweep knows whether it may run; a refusal must not leave one behind for
    // swx_recover_list to present as preserved work, or for the next reader to
    // mistake for a sweep that lost everything. `remove_dir` removes it only if
    // it is empty, so this can never delete a rescue.
    // A refusal that wrote nothing leaves no folder — and no manifest, since a
    // A refusal that wrote nothing leaves no folder — and no manifest, since a
    // manifest describing nothing is a rescue folder that looks like a rescue.
    // BUT A FAILURE IS SOMETHING TO SAY: once the record carries `failures`, a
    // sweep that preserved nothing because the host broke has the most important
    // thing to explain and no file to explain it beside, so the record is kept
    // whenever there is a failure in it.
    if recovered.is_empty() && close_failed.is_empty() {
        let _ = std::fs::remove_file(dir.join(MANIFEST_NAME));
    }
    let _ = std::fs::remove_dir(&dir);
    crate::ledger::record(
        "close_all_safely",
        json!({
            "recoveryDir": dir_s,
            "preserved": recovered.len(),
            "closed": closed,
            "reason": arg_str(args, "reason"),
            "authority": "Drew's standing authorization, 2026-09-11: the bridge closes documents \
                          itself, having first written every one of them to disk",
        }),
    );
    // SOLIDWORKS IS GONE — SAY SO. Measured 2026-10-01: SOLIDWORKS crashed in the
    // middle of a sweep and the reply said `dialog_census_unknown`, which is true
    // of a dead process and tells the reader nothing. When the sweep stopped
    // short (a host failure, or a census nobody could read) and SOLIDWORKS is no
    // longer running, the answer is `host_app_gone`: what was preserved (and
    // verified) before it went, what was closed, and what SOLIDWORKS wrote in its
    // performance log on the way down.
    let stopped_short = !close_failed.is_empty() || dialog_unreadable;
    if stopped_short && (before.is_some() || !recovered.is_empty()) && solidworks_gone(ctx) {
        let preserved = recovered.iter().filter(|r| r["preserved"] == true).count();
        let evidence = crate::perflog::crash_evidence();
        crate::ledger::record(
            "host_app_gone",
            json!({"during": "close_all_safely", "recoveryDir": dir_s, "preserved": preserved,
                   "closed": closed, "crashEvidence": evidence}),
        );
        let mut out = err_code(
            "host_app_gone",
            format!(
                "SOLIDWORKS stopped running during the sweep. Before it went, {preserved} \
                 document(s) were preserved (each verified on disk) and {closed} closed."
            ),
            "SOLIDWORKS is gone, not blocked: there is no dialog to look for. Tell the user what \
             was preserved and where (swx_recover_list), quote `crashEvidence` if it is not \
             empty, and let them restart SOLIDWORKS — its own Document Recovery may offer \
             more. Wait for swx_status to report `solidworks.ready` before any document verb.",
        );
        if let Value::Object(m) = &mut out {
            m.insert("recoveryDir".into(), json!(dir_s));
            m.insert("recovered".into(), json!(recovered));
            m.insert("preserved".into(), json!(preserved));
            m.insert("closed".into(), json!(closed));
            m.insert("closeFailed".into(), json!(close_failed));
            m.insert("crashEvidence".into(), json!(evidence));
            m.insert(
                "performanceLog".into(),
                crate::perflog::path().map(|p| json!(p.to_string_lossy())).unwrap_or(Value::Null),
            );
        }
        return out;
    }
    let remaining: Vec<Value> = recovered
        .iter()
        .filter(|r| r.get("preserved").and_then(|v| v.as_bool()) == Some(false))
        .cloned()
        .collect();
    let out = json!({
        "recoveryDir": dir_s,
        "recovered": recovered,
        "closed": closed,
        "remaining": remaining,
        "closeFailed": close_failed,
        // Components that went out WITH their assembly, each after its own
        // recovery write (0.9.12).
        "closedByParent": closed_by_parent,
        "documentCountBefore": before,
        "documentCountAfter": after,
        "done": done && stopped_by_dialog.is_none() && !dialog_unreadable
            && stopped_by_headroom.is_none(),
        // ALWAYS REPORTED, whether or not they stopped anything: a sweep that
        // writes every open document to disk and never says how much room it had
        // is asking to be trusted about the one thing it cannot verify afterwards.
        "headroomBytesBefore": free_before,
        "estimatedWriteBytes": estimate.total,
        "estimatedFrom": json!({
            "documents": open_paths.len(),
            "measured": estimate.measured,
            "estimated": estimate.estimated,
            "largestBytes": estimate.largest,
        }),
        "stoppedByDialog": stopped_by_dialog,
        // On screen and NOT blocking — reported with their evidence, never
        // answered (0.9.4).
        "notifications": notifications_now(ctx),
        // WHY it stopped, in one word a caller can branch on.
        "stoppedBy": if stopped_by_dialog.is_some() {
            json!("dialog")
        } else if stopped_by_headroom.is_some() {
            json!("insufficientHeadroom")
        } else if dialog_unreadable {
            json!("dialogCensusUnknown")
        } else {
            Value::Null
        },
        // Whether anybody could check for a dialog while this ran. The sweep does
        // not depend on it; the report says so rather than implying a check that
        // did not happen.
        "dialogCensusReadable": !dialog_unreadable,
        "windowEtiquette": "background",
        "progress": job.finished(),
    });
    if let Some(d) = out.get("stoppedByDialog").cloned().filter(|v| !v.is_null()) {
        let mut e = err_code(
            "stopped_by_dialog",
            format!("SOLIDWORKS raised a dialog during the sweep: {d}"),
            "NOTHING was answered and the sweep stopped. A save prompt should be impossible here — \
             every document is written to disk before anything closes — so a dialog means a premise \
             is wrong, and this bridge never answers one. swx_state takes a background screenshot; \
             show the user and let them answer, then run this again.",
        );
        if let Value::Object(m) = &mut e {
            for (k, v) in out.as_object().expect("object") {
                m.entry(k.clone()).or_insert(v.clone());
            }
        }
        return e;
    }
    if let Some(h) = stopped_by_headroom.clone() {
        let free = h.get("freeBytes").and_then(|v| v.as_u64()).unwrap_or(0);
        let need = h.get("neededBytes").and_then(|v| v.as_u64()).unwrap_or(0);
        let mut e = err_code(
            "insufficient_headroom",
            format!(
                "the recovery volume has {free} free byte(s) and this sweep needs about {need} \
                 ({} estimated for the session, doubled) — it {}",
                estimate.total,
                if recovered.is_empty() {
                    "wrote NOTHING and closed NOTHING".to_string()
                } else {
                    format!(
                        "stopped after preserving {} document(s) and closed NOTHING further",
                        recovered.len()
                    )
                }
            ),
            "A recovery file can be the RIGHT document CUT SHORT, and nothing in the file would \
             show it: the container marker sits at offset 4, so any prefix carries it, and a \
             truncation past 16 KB clears the size floor too. This bridge therefore refuses to \
             write a session it cannot fit rather than close documents against files it could not \
             have checked. Free space on the recovery volume, or point `recoveryDir` at one with \
             room, and run this again — nothing was lost and nothing was closed.",
        );
        if let Value::Object(m) = &mut e {
            m.insert("stoppedByHeadroom".into(), h);
            for (k, v) in out.as_object().expect("object") {
                m.entry(k.clone()).or_insert(v.clone());
            }
        }
        return e;
    }
    if dialog_unreadable {
        let mut e = err_code(
            "dialog_census_unknown",
            format!(
                "the window census could not be read, so this bridge cannot say whether a dialog \
                 is on screen — it PRESERVED {} document(s) to the recovery folder and closed NONE \
                 of them",
                recovered.iter().filter(|r| r["preserved"] == true).count()
            ),
            "Saving loses nothing, so the saves went ahead; closing while blind does not, so no \
             document was closed. A dialog nobody can see is exactly the one that matters — this is \
             the rule swx_stop applies to the same question. The work is already safe in \
             `recoveryDir` (swx_recover_list finds it). swx_status reports whether ab is reachable; \
             run this again when it is.",
        );
        if let Value::Object(m) = &mut e {
            for (k, v) in out.as_object().expect("object") {
                m.entry(k.clone()).or_insert(v.clone());
            }
        }
        return e;
    }
    ok_merge(out, json!({
        "_hint": if done {
            "Every open document was SAVED to the recovery folder and then closed; the session is \
             empty and the count proves it. Nothing of the user's was written over: a document that \
             already had a file was saved INTO recovery — the open document then pointed there, \
             which is what made the close key right, and their original file on disk was never \
             written to. `recoveryBytes` on each record is the size actually written, so a real \
             save can be told from a file that merely appeared. swx_recover_list shows what was \
             preserved, and swx_open reopens any of it."
        } else {
            "Documents are still open. `remaining` names the ones that could NOT be written to disk \
             — those were deliberately not closed, because closing a document whose only copy is in \
             memory is the one thing this verb exists to prevent. Fix what the reason says and run \
             it again."
        },
    }))
}

/// `swx_shutdown` — preserve everything, close everything, quit. One verb,
/// because "close the documents and then quit" is one intention and splitting it
/// across two calls is how a session ends up half-closed.
///
/// IDEMPOTENT: with SOLIDWORKS not running there is nothing to do and it says so
/// rather than failing, so a caller can always finish with it.
pub fn shutdown(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    if let Some(g) = require_reason(
        args,
        "swx_shutdown",
        "it saves every open document to a recovery folder, closes them, and quits SOLIDWORKS",
    ) {
        return g;
    }
    // NOTHING TO DO IS NOT A FAILURE.
    if let Ok(st) = ctx.call("status", json!({}), 20) {
        if st.get("running").and_then(|v| v.as_bool()) == Some(false) {
            return ok(json!({
                "alreadyStopped": true,
                "closed": 0,
                "recovered": [],
                "stopped": false,
                "_hint": "SOLIDWORKS is not running, so nothing was closed and nothing was quit.                           This verb is idempotent on purpose: it is the last thing a caller does,                           and the last thing should be safe to repeat.",
            }));
        }
    }
    let swept = close_all_safely(ctx, args);
    if swept.get("success").and_then(|v| v.as_bool()) != Some(true)
        || swept.get("done").and_then(|v| v.as_bool()) != Some(true)
    {
        // THE SWEEP DID NOT EMPTY THE SESSION, so the quit is not attempted: a
        // quit with documents open is the outcome swx_stop exists to refuse, and
        // reaching it through this verb would be a way round that rule.
        let mut out = swept;
        if let Value::Object(m) = &mut out {
            m.insert("stopped".into(), json!(false));
            m.insert(
                "quitSkipped".into(),
                json!("the session was not emptied, so SOLIDWORKS was not asked to quit"),
            );
        }
        return out;
    }
    let stop = ctx_stop(ctx, args);
    let mut out = swept;
    if let Value::Object(m) = &mut out {
        m.insert("stopped".into(), stop.get("stopped").cloned().unwrap_or(json!(false)));
        m.insert("quit".into(), stop);
        m.insert(
            "_hint".into(),
            json!(
                "Every open document was written to the recovery folder, then closed, then                  SOLIDWORKS was asked to quit — in that order, which is the whole safety property:                  nothing closes before it is on disk. `recovered` lists what was preserved and                  where; swx_recover_list finds it again later. On a Connected edition starting                  SOLIDWORKS again takes 1-3 minutes and may stop at a sign-in only the user can                  answer."
            ),
        );
    }
    out
}

/// The quit, under the standing authorization.
fn ctx_stop(ctx: &Ctx, args: &Value) -> Value {
    let mut a = args.clone();
    if let Value::Object(m) = &mut a {
        m.insert("userAuthorized".into(), json!(true));
        m.insert("confirm".into(), json!(true));
        m.insert("allowUserWrites".into(), json!(true));
        m.insert("force".into(), json!(true));
    }
    crate::verbs::dispatch(ctx, "swx_stop", &a)
}

/// The base name of a copy kept BESIDE another by the migration —
/// `x.SLDPRT.legacy-20261001-003000[-N]` → `x.SLDPRT` — or `None` for any other
/// name.
fn beside_base(name: &str) -> Option<&str> {
    let i = name.rfind(".legacy-")?;
    let tail = &name[i + ".legacy-".len()..];
    let stamp_like = tail.len() >= 15
        && tail.chars().all(|c| c.is_ascii_digit() || c == '-')
        && tail.as_bytes()[8] == b'-';
    (stamp_like && i > 0).then(|| &name[..i])
}

/// ONE DOCUMENT A SWEEP CLOSED IN PLACE, as the listing reports it — or `None`
/// for a manifest entry that is not one.
///
/// A document that was clean and saved when a sweep reached it needed no copy:
/// its own file IS its preservation, so the sweep wrote nothing for it and closed
/// it at its own path — a file no sweep ever writes to. That leaves it with no
/// file in the recovery folder, which is exactly why it needs an entry here:
/// "where is my file?" must not come back empty for a document that is sitting,
/// untouched, at its own path. Measured live 2026-09-30: the record had nothing
/// to say about such a document at all.
///
/// "UNTOUCHED" IS MEASURED, NOT ASSUMED. The sweep recorded the file's size and
/// modification time when it closed the document; this compares them with what
/// is on disk now, and says which of the three it is — the same, changed since
/// (the user reopened and saved it, most likely), or gone from its path.
fn closed_in_place_entry(e: &Value) -> Option<Value> {
    let flagged = e.get("closedInPlace").and_then(|v| v.as_bool());
    let original = e.get("originalPath").and_then(|v| v.as_str()).unwrap_or("").to_string();
    if original.trim().is_empty() {
        return None;
    }
    // A RECORD FROM BEFORE 0.9.0 carries no flag, but the same facts identify
    // such a document: the sweep called it preserved, wrote no copy, and it had
    // a path of its own.
    let legacy_shape = flagged.is_none()
        && e.get("recoveryPath").map(|v| v.is_null()).unwrap_or(true)
        && e.get("preserved").and_then(|v| v.as_bool()) == Some(true);
    if !(flagged == Some(true) || legacy_shape) {
        return None;
    }
    let meta = std::fs::metadata(&original).ok().filter(|m| m.is_file());
    let present = meta.is_some();
    let bytes_now = meta.as_ref().map(|m| m.len());
    let modified_now = if present { crate::scratch::modified_ms(&original) } else { None };
    let bytes_then = e.get("originalBytes").and_then(|v| v.as_u64());
    let modified_then = e.get("originalModifiedMs").and_then(|v| v.as_u64());
    let differs = matches!((bytes_then, bytes_now), (Some(a), Some(b)) if a != b)
        || matches!((modified_then, modified_now), (Some(a), Some(b)) if a != b);
    let comparable = (bytes_then.is_some() && bytes_now.is_some())
        || (modified_then.is_some() && modified_now.is_some());
    // null when there is nothing to compare: no measurement then, or no file now.
    let untouched: Option<bool> = if !present {
        None
    } else if differs {
        Some(false)
    } else if comparable {
        Some(true)
    } else {
        None
    };
    // CLOSED, as the sweep recorded it. A pre-0.9.0 record computed `closed` from
    // the recovery path — which a document closed in place does not have — so it
    // always said false; that is not evidence, and it is reported as unknown.
    let closed = if flagged == Some(true) {
        e.get("closed").and_then(|v| v.as_bool())
    } else {
        None
    };
    let closed_words = match closed {
        Some(true) => format!("it was closed at its own path, {original}"),
        Some(false) => format!(
            "this record does NOT show it closed — it may still be open in SOLIDWORKS, at \
             its own path, {original}"
        ),
        None => format!(
            "its own path is {original} (this record was written by a bridge older than 0.9.0, \
             which did not record whether a document it closed in place was closed)"
        ),
    };
    let file_words = match (present, untouched) {
        (false, _) => format!(
            "NOTHING IS AT {original} NOW: something moved or deleted that file after the sweep. \
             The sweep never writes to a document's own file and, since the document was clean, \
             kept no copy of it — there is none in this folder."
        ),
        (true, Some(true)) => format!(
            "The file is exactly as the sweep found it ({} byte(s)): untouched. Open it from \
             there with swx_open.",
            bytes_now.unwrap_or(0)
        ),
        (true, Some(false)) => format!(
            "The file HAS CHANGED since the sweep ({}). The sweep never writes to a document's \
             own file, so something else saved it afterwards — most often SOLIDWORKS, after the \
             document was reopened. Open it from there with swx_open.",
            match (bytes_then, bytes_now) {
                (Some(a), Some(b)) if a != b => format!("{a} byte(s) then, {b} now"),
                _ => "its modification time is later than the one recorded".to_string(),
            }
        ),
        (true, None) => "The sweep did not measure the file when it closed the document, so this \
                         bridge cannot say whether it has changed since; the sweep itself never \
                         writes to a document's own file. Open it from there with swx_open."
            .to_string(),
    };
    let headline = match (present, untouched, closed) {
        (false, _, _) => "NOT AT ITS PATH NOW.",
        (true, Some(true), Some(true)) => "CLOSED IN PLACE, UNTOUCHED.",
        (true, Some(false), _) => "AT ITS OWN PATH, CHANGED SINCE.",
        _ => "AT ITS OWN PATH.",
    };
    Some(json!({
        "closedInPlace": true,
        "source": "manifest",
        "title": e.get("title").cloned().unwrap_or(Value::Null),
        "originalPath": original,
        "kind": e.get("kind").cloned().unwrap_or(Value::Null),
        // No copy exists, by design: the document's own file was its preservation.
        "recoveryPath": Value::Null,
        "closed": closed,
        "present": present,
        "originalBytes": bytes_then,
        "originalModifiedMs": modified_then,
        "bytesNow": bytes_now,
        "modifiedMsNow": modified_now,
        "untouched": untouched,
        "howToOpen": format!(
            "{headline} This document was clean and saved when the sweep reached it, so no copy \
             was needed: {closed_words}. {file_words}"
        ),
    }))
}

/// IS SOLIDWORKS STILL THERE? Asked only after a sweep stopped short. COM first
/// (the host's `startup_state` reports `running:false` when there is no
/// SOLIDWORKS to attach to); if the host cannot answer, the process census. An
/// unreadable census is NOT "gone".
fn solidworks_gone(ctx: &Ctx) -> bool {
    match ctx.call("startup_state", json!({}), 20) {
        Ok(s) => s.get("running").and_then(|v| v.as_bool()) == Some(false),
        Err(_) => crate::launchos::os()
            .processes()
            .map(|ps| !ps.iter().any(|p| p.is("sldworks.exe")))
            .unwrap_or(false),
    }
}

/// `swx_recover_list` — what was preserved, and where. Read-only.
pub fn recover_list(ctx: &Ctx, _args: &Value) -> Value {
    let _ = ctx;
    let root = crate::scratch::root().join("recovery");
    // THE LEGACY LOCATIONS TOO, oldest first: 0.8.x's install cache, which an
    // update re-extracts, and 0.9.0's `Adom Bridge\solidworks-data`, inside the
    // directory ab's installer owns. Anything in them is migrated on start — but
    // a rescue an older bridge wrote after that, or one whose copy failed
    // verification, exists ONLY there, and a listing that did not look would
    // tell the user their work was gone while it was sitting one directory over.
    let legacies: Vec<(std::path::PathBuf, &'static str)> = crate::scratch::legacy_roots()
        .into_iter()
        .map(|l| l.join("recovery"))
        .filter(|l| l != &root)
        .enumerate()
        .map(|(i, l)| {
            (l, match i {
                0 => "legacyInstallCache",
                1 => "legacyDataRoot",
                _ => "legacy",
            })
        })
        .collect();
    // WHERE AN OLDER LOCATION STILL HOLDS a file of the same sweep and name — the
    // original of a copy in the data root, if there is one left.
    let original_of = |sweep: &std::ffi::OsStr, name: &str| -> Option<std::path::PathBuf> {
        legacies.iter().map(|(l, _)| l.join(sweep).join(name)).find(|p| p.is_file())
    };
    let mut sweeps: Vec<Value> = Vec::new();
    let mut dirs: Vec<(std::path::PathBuf, &'static str)> = Vec::new();
    for (base, location) in std::iter::once((root.clone(), "data")).chain(legacies.iter().cloned()) {
        if let Ok(entries) = std::fs::read_dir(&base) {
            dirs.extend(
                entries.flatten().map(|e| e.path()).filter(|p| p.is_dir()).map(|p| (p, location)),
            );
        }
    }
    // Newest first, across every location.
    dirs.sort_by(|a, b| b.0.file_name().cmp(&a.0.file_name()));
    {
        for (d, location) in dirs {
            let mut files: Vec<Value> = Vec::new();
            let mut lock_files: Vec<Value> = Vec::new();
            // THE SWEEP'S OWN RECORD, IF IT LEFT ONE. Re-deriving a verdict from
            // the file alone cannot see the two facts that decide the hard cases:
            // what the original document weighed, and whether the extension was a
            // GUESS. Both were known at save time. A manifest carries them; where
            // there is none, this says so rather than presenting a weaker check as
            // the same answer.
            let manifest: Option<Value> = std::fs::read_to_string(d.join(MANIFEST_NAME))
                .ok()
                .and_then(|t| serde_json::from_str::<Value>(&t).ok())
                // A SHAPE CHECK IS NOT A VERSION CHECK. `files` being an array
                // says nothing about what its entries mean; a future manifest
                // with different field semantics would be read as this one.
                .filter(|m| {
                    m.get("files").map(|f| f.is_array()).unwrap_or(false)
                        && m.get("manifestVersion").and_then(|v| v.as_u64()) == Some(1)
                });
            let entries: Vec<Value> = manifest
                .as_ref()
                .and_then(|m| m.get("files").and_then(|f| f.as_array()).cloned())
                .unwrap_or_default();
            if let Ok(fs) = std::fs::read_dir(&d) {
                for f in fs.flatten() {
                    let p = f.path();
                    if !p.is_file() {
                        continue;
                    }
                    let name = p
                        .file_name()
                        .map(|n| n.to_string_lossy().to_string())
                        .unwrap_or_default();
                    // The record is not one of the rescues.
                    if name == MANIFEST_NAME || name == "manifest.json.tmp" {
                        continue;
                    }
                    // NOR IS A LOCK FILE. SOLIDWORKS keeps `~$<name>` beside a
                    // document while it is open — and a rescue IS open at its
                    // recovery path until the sweep closes it, or forever if
                    // SOLIDWORKS died first (measured 2026-10-01). Listing it as an
                    // unverified file read as a failed rescue. It is named apart.
                    if name.starts_with("~$") {
                        lock_files.push(json!({
                            "name": name,
                            "lockFile": true,
                            "lockOf": &name[2..],
                            "note": "SOLIDWORKS' lock file for a document open at this path: not a \
                                     rescue, and not a failed one. It disappears when the document \
                                     is closed; one left behind means SOLIDWORKS stopped while it \
                                     was open.",
                        }));
                        continue;
                    }
                    let bytes = f.metadata().ok().map(|m| m.len());
                    // A MIGRATION'S HALF-COPIED TEMPORARY (see `migrate_legacy`):
                    // incomplete by definition, so it is never a rescue — but it
                    // is not hidden either. It only survives a later start when
                    // the original it was copying is gone from ab's install cache,
                    // and then these may be the only bytes of that file left.
                    if let Some(real) = name.strip_suffix(".migrating") {
                        let original = if location == "data" {
                            d.file_name().and_then(|sweep| original_of(sweep, real))
                        } else {
                            None
                        };
                        files.push(json!({
                            "path": p.to_string_lossy(),
                            "name": name,
                            "bytes": bytes,
                            "source": "migration",
                            "partialCopy": true,
                            "copyOf": real,
                            "legacyPath": original.as_ref().map(|o| json!(o.to_string_lossy())).unwrap_or(Value::Null),
                            "verified": false,
                            "verifiedReason": "an interrupted migration's temporary file: incomplete by \
                                               definition",
                            "howToOpen": match &original {
                                Some(o) => format!(
                                    "INTERRUPTED COPY of {real}: these bytes are incomplete. The \
                                     original is still at {}, and the bridge copies it again on \
                                     its next start.",
                                    o.display()
                                ),
                                None => format!(
                                    "PARTIAL COPY of {real}: a migration out of an older bridge's \
                                     location was interrupted while copying it, and the original is \
                                     no longer there. These bytes are INCOMPLETE — SOLIDWORKS is \
                                     unlikely to open them — and are kept only because they may be \
                                     the only bytes of that file left."
                                ),
                            },
                        }));
                        continue;
                    }
                    // A COPY KEPT BESIDE ANOTHER (see `migrate_legacy`): a later
                    // version of a file found in an older location after its first
                    // copy, or a different file of the same name. Its name carries
                    // `.legacy-<stamp>`, so it is verified as the kind its base name
                    // says — and it is said to be what it is.
                    if let Some(base) = beside_base(&name) {
                        // A RECORD kept beside a record is not a rescue either.
                        if base == MANIFEST_NAME {
                            continue;
                        }
                        let kind = crate::disk::DocKind::from_extension(base);
                        let v = crate::comhost::host::recovery_file_verdict(
                            &p.to_string_lossy(),
                            kind,
                            None,
                        );
                        let mut e = reverified_entry(&p, &name, bytes, kind, &v);
                        e["copiedBeside"] = json!(true);
                        e["besideOf"] = json!(base);
                        e["howToOpen"] = json!(format!(
                            "A COPY KEPT BESIDE {base}: an older bridge's location held a \
                             DIFFERENT version of {base} after it had been copied once (or a \
                             different file of that name), so it was copied again under this \
                             name rather than over the first. Check both before relying on \
                             either. {}",
                            e["howToOpen"].as_str().unwrap_or("")
                        ));
                        files.push(e);
                        continue;
                    }
                    let entry = entries.iter().find(|e| {
                        e.get("name").and_then(|v| v.as_str()).map(|n| n == name).unwrap_or(false)
                    });
                    files.push(match entry {
                        // ── THE SWEEP DECIDED THIS, and it knew things this verb
                        //    cannot re-derive. Report what it decided.
                        Some(e) => {
                            // THE VERDICT IS ABOUT BYTES, NOT ABOUT A NAME.
                            //
                            // The manifest records the size the sweep verified,
                            // and this verb has just read the size on disk. If
                            // they differ, the file CHANGED after the sweep — a
                            // disk that filled, an anti-virus quarantine, a sync
                            // client rewriting it — and the sweep's verdict is
                            // about bytes that are no longer there. Reporting it
                            // anyway is the worst version of this defect:
                            // `verified:true` in a manifest means the DOCUMENT
                            // WAS CLOSED, so this file is the only copy of that
                            // work and the user would be told it is intact. A
                            // vanished file is already `present:false`; an
                            // emptied one is the same event, and takes the same
                            // answer the reverified path gives — UNKNOWN.
                            // AN ENTRY THAT CANNOT BE BOUND TO THESE BYTES IS
                            // UNKNOWN, not "unchanged". `_ => false` read a
                            // MISSING or null `recoveryBytes` as agreement, so a
                            // manifest verdict was handed to a 900-byte file it
                            // was never about — and `verified:true` in a manifest
                            // means the document was CLOSED, so that file is the
                            // only copy of the work. Unbindable takes the same
                            // answer the reverified path gives.
                            let recorded = e.get("recoveryBytes").and_then(|v| v.as_u64());
                            enum Binding {
                                Same,
                                Changed,
                                Unbindable,
                            }
                            let binding = match (recorded, bytes) {
                                (Some(was), Some(now)) if was == now => Binding::Same,
                                (Some(_), Some(_)) => Binding::Changed,
                                // Either the record does not say what it verified,
                                // or this file's size cannot be read now.
                                _ => Binding::Unbindable,
                            };
                            let changed = matches!(binding, Binding::Changed);
                            let verified = match binding {
                                Binding::Same => {
                                    Some(e.get("verified").and_then(|v| v.as_bool()).unwrap_or(false))
                                }
                                _ => None,
                            };
                            let reason = if matches!(binding, Binding::Unbindable) {
                                format!(
                                    "this record cannot be tied to these bytes: it {} and the file \
                                     {}. A verdict about bytes nobody can match to this file is \
                                     not a verdict about this file.",
                                    match recorded {
                                        Some(n) => format!("verified {n} byte(s)"),
                                        None => "does not say how many bytes it verified".into(),
                                    },
                                    match bytes {
                                        Some(n) => format!("holds {n}"),
                                        None => "cannot be measured now".into(),
                                    }
                                )
                            } else if changed {
                                format!(
                                    "the file changed since the sweep: it verified {} byte(s) and \
                                     holds {} now. Whatever the sweep decided was about bytes that \
                                     are no longer there, so this bridge cannot vouch for what is.",
                                    recorded.unwrap_or(0),
                                    bytes.unwrap_or(0)
                                )
                            } else {
                                e.get("verdictReason")
                                    .and_then(|v| v.as_str())
                                    .unwrap_or("")
                                    .to_string()
                            };
                            let fallback = e
                                .get("namingFallback")
                                .and_then(|v| v.as_str())
                                .map(|x| x.to_string());
                            let kind_known = e.get("kind").map(|k| !k.is_null()).unwrap_or(false);
                            json!({
                                "path": p.to_string_lossy(),
                                "name": name,
                                "bytes": bytes,
                                "source": "manifest",
                                "title": e.get("title").cloned().unwrap_or(Value::Null),
                                "originalPath": e.get("originalPath").cloned().unwrap_or(Value::Null),
                                "kind": e.get("kind").cloned().unwrap_or(Value::Null),
                                "kindKnown": kind_known,
                                // THE NAME WAS A GUESS, and no file name can say so.
                                "namingFallback": fallback,
                                "originalBytes": e.get("originalBytes").cloned().unwrap_or(Value::Null),
                                "signatureOk": e.get("signatureOk").cloned().unwrap_or(Value::Null),
                                "closed": e.get("closed").cloned().unwrap_or(Value::Null),
                                "verified": verified,
                                "recoveryBytes": recorded,
                                "changedSinceSweep": changed,
                                "verifiedReason": if reason.is_empty() { Value::Null } else { json!(reason) },
                                "howToOpen": match verified {
                                    Some(v) => how_to_open(v, kind_known, &reason, fallback.is_some()),
                                    // CHANGED UNDERNEATH THE RECORD. Not a
                                    // refusal by the sweep, and not a rescue this
                                    // bridge can stand behind.
                                    None => format!(
                                        "UNCONFIRMED. {reason} The document this came from was \
                                         closed against the file as it WAS, so treat these bytes \
                                         as the only copy and check them before relying on them: \
                                         try swx_open (renaming a `.sldrecover` file first), and \
                                         if SOLIDWORKS refuses it, the work is gone from here."
                                    ),
                                },
                            })
                        }
                        // ── NO RECORD FOR THIS FILE. Verify what can be verified
                        //    and be explicit that it is less.
                        None => {
                            let kind = crate::disk::DocKind::from_extension(&name);
                            let v = crate::comhost::host::recovery_file_verdict(
                                &p.to_string_lossy(),
                                kind,
                                None,
                            );
                            reverified_entry(&p, &name, bytes, kind, &v)
                        }
                    });
                }
            }
            // A FILE THE SWEEP WROTE THAT IS NO LONGER THERE. Somebody moved or
            // deleted it; saying nothing would let a rescue disappear silently.
            for e in &entries {
                let name = e.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
                // Listed already, under its own name.
                if name.is_empty() || files.iter().any(|f| f["name"] == json!(name)) {
                    continue;
                }
                // NOT GONE — NOT COPIED YET. A sweep migrated from ab's install
                // cache carries its manifest into the data root even when one of
                // its files did not copy (the copy failed verification, or the
                // start that was copying it died). That file is still in the old
                // location, and calling it deleted would tell the user their work
                // was lost while it sits one directory over, waiting for the next
                // update to take it.
                let at_legacy = if location == "data" {
                    d.file_name().and_then(|sweep| original_of(sweep, &name))
                } else {
                    None
                };
                // ONLY A PARTIAL COPY OF IT EXISTS HERE — an interrupted migration
                // left `<name>.migrating` beside where it should be. That is not
                // "moved or deleted by something else": the copy never finished,
                // and the entry says so, pointing at the partial bytes and at the
                // original if an older location still has it.
                let partial = files
                    .iter()
                    .find(|f| f["partialCopy"] == true && f["copyOf"] == json!(name))
                    .map(|f| f["name"].clone());
                if let Some(partial_name) = partial {
                    let partial_name = partial_name.as_str().unwrap_or("").to_string();
                    files.push(json!({
                        "path": Value::Null,
                        "name": name,
                        "bytes": Value::Null,
                        "source": "manifest",
                        "present": false,
                        "partialCopyOnly": true,
                        "partialCopy": partial_name,
                        "legacyPath": at_legacy.as_ref().map(|o| json!(o.to_string_lossy())).unwrap_or(Value::Null),
                        "title": e.get("title").cloned().unwrap_or(Value::Null),
                        "verified": false,
                        "verifiedReason": "the copy of this file out of an older bridge's location \
                                           never finished; only incomplete bytes are here",
                        "howToOpen": match &at_legacy {
                            Some(o) => format!(
                                "NOT COPIED YET — AN EARLIER COPY WAS INTERRUPTED. {partial_name} \
                                 holds incomplete bytes; the whole original is still at {}, and \
                                 the bridge copies it again on its next start. If this file \
                                 matters, copy the original out of that folder now.",
                                o.display()
                            ),
                            None => format!(
                                "ONLY A PARTIAL COPY EXISTS. Copying this file out of an older \
                                 bridge's location was interrupted, and the original is no \
                                 longer there. Nothing moved or deleted it from this folder — \
                                 the copy never finished — and {partial_name}, beside it, holds \
                                 the INCOMPLETE bytes that remain."
                            ),
                        },
                    }));
                    continue;
                }
                if let Some(original) = at_legacy {
                    files.push(json!({
                        "path": Value::Null,
                        "name": name,
                        "bytes": Value::Null,
                        "source": "manifest",
                        "present": false,
                        "notCopiedYet": true,
                        "legacyPath": original.to_string_lossy(),
                        "title": e.get("title").cloned().unwrap_or(Value::Null),
                        "verified": Value::Null,
                        "verifiedReason": "this file has not been copied out of an older bridge's \
                                           location yet — its copy failed verification or was \
                                           interrupted — so the only copy is at `legacyPath`",
                        "howToOpen": format!(
                            "NOT COPIED YET. The only copy of this file is still at {}, inside \
                             ab's directory, where an update of the bridge or of ab can DELETE \
                             it. The bridge retries the copy on its next start; if this file \
                             matters, copy it out of that folder now.",
                            original.display()
                        ),
                    }));
                    continue;
                }
                files.push(json!({
                    "path": e.get("recoveryPath").cloned().unwrap_or(Value::Null),
                    "name": name,
                    "bytes": Value::Null,
                    "source": "manifest",
                    "present": false,
                    "title": e.get("title").cloned().unwrap_or(Value::Null),
                    "verified": false,
                    "verifiedReason": "the sweep wrote this file and it is no longer in the \
                                       recovery folder",
                    "howToOpen": "GONE. This bridge wrote this file during a sweep and it is not \
                                  there now — it was moved or deleted by something else. If the \
                                  document it came from was closed, these were its only bytes.",
                }));
            }
            files.sort_by_key(|f| f.get("name").and_then(|n| n.as_str()).unwrap_or("").to_string());
            // DOCUMENTS CLOSED AT THEIR OWN PATHS: no file here, by design, and
            // listed anyway — see `closed_in_place_entry`.
            let in_place: Vec<Value> = entries.iter().filter_map(|e| closed_in_place_entry(e)).collect();
            let endangered = location != "data";
            let from_update = location == "legacyInstallCache";
            // ALREADY COPIED OUT? The migration leaves the originals where they
            // were, so after it runs the same sweep is listed twice — and the two
            // must not read the same. A legacy sweep whose every file has a
            // same-sized counterpart under the data root says where that copy is;
            // one that does not is the only copy there is, and says THAT.
            let copied_to: Option<std::path::PathBuf> = if endangered {
                d.file_name().map(|n| root.join(n)).filter(|twin| {
                    twin.is_dir()
                        && std::fs::read_dir(&d)
                            .map(|rd| {
                                rd.flatten().filter(|e| e.path().is_file()).all(|e| {
                                    let theirs = e.metadata().ok().map(|m| m.len());
                                    let ours = std::fs::metadata(twin.join(e.file_name()))
                                        .ok()
                                        .filter(|m| m.is_file())
                                        .map(|m| m.len());
                                    theirs.is_some() && theirs == ours
                                })
                            })
                            .unwrap_or(false)
                })
            } else {
                None
            };
            // WHAT CAN TAKE IT: the bridge cache is re-extracted by every update
            // of the bridge; ab's own directory is ab's installer's and
            // uninstaller's, and what they do to it is undocumented.
            let threat = if from_update {
                "ab's INSTALL CACHE, which is re-extracted — and emptied — whenever the bridge \
                 updates"
            } else {
                "ab's own directory, which ab's installer and uninstaller own — what an ab \
                 uninstall, repair or reinstall does to it is undocumented"
            };
            sweeps.push(json!({
                "sweep": d.file_name().map(|n| n.to_string_lossy().to_string()),
                "directory": d.to_string_lossy(),
                // WHERE IT LIVES, because the older places belong to something
                // else.
                "location": location,
                "atRisk": endangered,
                "atRiskFromUpdate": from_update,
                "copiedTo": copied_to.as_ref().map(|c| json!(c.to_string_lossy())).unwrap_or(Value::Null),
                "warning": match (endangered, &copied_to) {
                    (false, _) => Value::Null,
                    (true, Some(c)) => json!(format!(
                        "This is the ORIGINAL of a rescue written by an older bridge, left in \
                         {threat}. Every file in it has a copy in the data root, at {}: use that \
                         one.",
                        c.display()
                    )),
                    (true, None) => json!(format!(
                        "This rescue is in {threat}, and there is NO copy of it in the data root. \
                         It was written by an older bridge. Copy anything you need out of it now; \
                         the bridge retries the migration on its next start and never deletes the \
                         original."
                    )),
                },
                "files": files,
                "lockFiles": lock_files,
                "closedInPlace": in_place,
            }));
        }
    }
    // THE MIGRATION'S OWN RECORD: what changed in an older location after it was
    // copied (and was copied again, beside the first copy), and what is still
    // failing to copy. Everything in it is about files whose originals live where
    // something else decides their fate.
    let marker: Value = std::fs::read_to_string(crate::scratch::root().join("migrated-from-legacy.json"))
        .ok()
        .and_then(|t| serde_json::from_str::<Value>(&t).ok())
        .unwrap_or(Value::Null);
    let migration = if marker.is_null() {
        Value::Null
    } else {
        json!({
            "changedSinceCopy": marker.get("changedSinceCopy").cloned().unwrap_or(json!([])),
            "merged": marker.get("merged").cloned().unwrap_or(json!([])),
            "mergeFailed": marker.get("mergeFailed").cloned().unwrap_or(json!([])),
            "failed": marker.get("failed").cloned().unwrap_or(json!([])),
            "lastRunAtMs": marker.get("lastRunAtMs").cloned().unwrap_or(Value::Null),
            "_note": "changedSinceCopy: a file in an older bridge's location that changed AFTER \
                      it was copied here (a rollback's ledger entries, a rewritten registry). It \
                      was copied again, beside the first copy (`copiedAs`), never over it. merged: \
                      the ledger or the ownership registry existed in two versions (two older \
                      locations, or a change after the copy), and the live file is now their \
                      UNION — ledger lines once each in `at` order, registry claims once each — \
                      with both versions kept beside it. mergeFailed: a version that could not \
                      be merged, kept beside instead. failed: files still only in the older \
                      location; the bridge retries them on start.",
        })
    };
    ok(json!({
        "recoveryRoot": root.to_string_lossy(),
        "legacyRecoveryRoot": legacies.first().map(|(l, _)| json!(l.to_string_lossy())).unwrap_or(Value::Null),
        "legacyRecoveryRoots": legacies.iter().map(|(l, loc)| json!({"path": l.to_string_lossy(), "location": loc})).collect::<Vec<_>>(),
        "migration": migration,
        "sweeps": sweeps,
        // THE PER-FILE INSTRUCTION IS THE ONE TO FOLLOW. A blanket "reopen with
        // swx_open" was wrong for every .sldrecover file in the list, so the
        // general hint now points at the specific one rather than competing with
        // it.
        // WHAT IS IN HERE IS NOT ALL RESCUED WORK, and the headline must not
        // say it is. A sweep that preserved NOTHING still leaves files behind.
        "_hint": "Newest sweep first. **Files marked `verified:false` are NOT complete documents** \
                  — the sweep refused to close against them and left the document OPEN, so that \
                  work is still in SOLIDWORKS rather than here; their bytes are kept only because \
                  they are the only ones there are. A file marked `verified:true` was written and \
                  checked before its document was closed. Follow each file's own `howToOpen` \
                  rather than assuming swx_open works: a verified file whose kind could not be \
                  established is named `.sldrecover`, and SOLIDWORKS will not open it until it is \
                  renamed. Nothing in here is deleted by this bridge except by swx_uninstall — a \
                  verified recovery file is the user's only copy of work they had not saved. A \
                  document that was clean and saved needed no copy, so it has no file here: it is \
                  listed under its sweep's `closedInPlace`, closed at its own path — a file no \
                  sweep writes to — with whether that file is still untouched.",
    }))
}

/// `swx_close_all_documents` — destructive, so it is confirm-gated.
pub fn close_all_documents(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    if let Err(e) = identity_claim_ok(ctx) {
        return e;
    }
    if let Some(g) = require_confirm(args, "swx_close_all_documents", "close EVERY open SOLIDWORKS document, including ones the user opened themselves") {
        return g;
    }
    let close_unsaved = arg_bool(args, "closeUnsaved", false);
    // `closeUnsaved:true` DISCARDS work. A single dirty document needs confirm
    // on swx_close_doc; it would be incoherent for one flag here to throw away
    // every dirty document the user has, with no reason recorded and no
    // per-document accounting. So the destructive form is gated harder, and
    // dirty documents the bridge did not create are NOT discarded at all —
    // they are closed one at a time only when they are ours.
    if close_unsaved {
        if let Some(g) = require_reason(args, "swx_close_all_documents {closeUnsaved:true}", "this discards unsaved work in every open document") {
            return g;
        }
    }
    // AUTHORIZE BEFORE ACTING. This check used to sit AFTER the loop below, so
    // in the default posture the verb closed the bridge's own dirty documents
    // and THEN returned a refusal — side effects on the way to "no". The grant
    // it produces is also what the per-document closes need, so there was never
    // a reason for it to be second.
    let grant = match crate::writes::authorize_in(
        ctx,
        args,
        "swx_close_all_documents",
        crate::writes::Kind::EndSession,
        None,
        None,
    ) {
        Ok(g) => g,
        Err(e) => return e,
    };
    let (session, open, _active, truncated) = ownership_context(ctx);
    prune_unless_truncated(ctx, session, &open, truncated);

    // A document SOLIDWORKS will not answer for is treated as dirty. `dirty`
    // absent or null used to be read as clean via `unwrap_or(false)`, so such a
    // document never entered the loop below, `CloseAllDocuments(false)` then
    // left it alone because it needed saving, and it came back in the host's
    // `remaining` with no per-document explanation anywhere in the response.
    // That is the same rule swx_close_doc already applies: an unanswerable
    // question is not permission.
    let dirty: Vec<&Value> = open
        .iter()
        .filter(|d| d.get("dirty").and_then(|x| x.as_bool()) != Some(false))
        .collect();
    let mut skipped = Vec::new();
    let mut discarded = Vec::new();
    if close_unsaved {
        // Close the dirty documents individually, then let CloseAllDocuments
        // take the clean remainder. OURS close without asking; the USER'S are
        // spared unless the full opt-in is present — and when it IS present,
        // they close, because refusing what the human explicitly authorized is
        // not safety. Live on 2026-09-03 this branch skipped them
        // unconditionally, so `closeUnsaved:true` with the opt-in closed 3 of 5
        // documents and left two behind.
        for d in &dirty {
            let title = d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();
            let path = d.get("path").and_then(|p| p.as_str()).filter(|s| !s.is_empty());
            let owned = ctx.owns_doc(&title, path, session);
            // A PATHLESS DOCUMENT IS CLOSED BY ITS TITLE, and `CloseDoc` resolves
            // a title case-insensitively — so this is the same hazard the drain
            // guards, in the other verb that reaches it. Without the guard, our
            // untitled `Part1.SLDPRT` beside the user's saved `part1.SLDPRT` would
            // hand SOLIDWORKS a name that matches both and let it choose.
            if path.is_none() && !title_resolves_uniquely(&open, &title) {
                skipped.push(json!({
                    "title": title,
                    "path": Value::Null,
                    "dirty": true,
                    "why": "this document has NO PATH, so its title is the only name it has — and \
                            another open document answers to that name too (SOLIDWORKS resolves a \
                            title without regard to case). Nothing was attempted: no API call can \
                            say which one is meant. Close it in SOLIDWORKS, or save one of them \
                            under another name.",
                }));
                continue;
            }
            // NO SECOND SERVER-SIDE CHECK HERE, and the reason is worth stating:
            // reaching this line already required the FULL opt-in (the
            // session-ending grant above), which is the identical condition a
            // per-document `Kind::CloseDocument` check would apply. Writing that
            // check anyway would have looked like a guard while being incapable
            // of ever refusing — a branch no test could reach, which is exactly
            // the kind of decoration that made five rounds of review necessary.
            // What DOES still guard this is the com host, whose record of the
            // documents it created is its own; a refusal from there lands in
            // `skipped` below.
            let call_args = crate::writes::with_grant(
                json!({"docName": title.clone(), "path": path, "save": false}),
                &grant,
            );
            match ctx.call("close_doc", call_args, 60) {
                Ok(_) => {
                    ctx.forget_scratch_doc(&title, path.as_deref());
                    crate::ledger::record(
                        "document_closed",
                        json!({"title": title, "bridgeCreated": owned, "by": "close_all_documents"}),
                    );
                    if !owned {
                        crate::ledger::record(
                            "user_document_changes_discarded",
                            json!({"document": title, "path": path, "via": "swx_close_all_documents", "reason": arg_str(args, "reason")}),
                        );
                    }
                    discarded.push(title);
                }
                Err(e) => skipped.push(json!({
                    "title": title,
                    "path": path,
                    "bridgeCreated": owned,
                    "errorCode": e.code,
                    "why": e.message,
                    "detail": e.detail,
                    // The structured COM failure, where the host had one.
                    "hostError": e.data.clone().unwrap_or(Value::Null),
                })),
            }
        }
    }
    // Whatever is left is closed WITHOUT discarding anything: CloseAllDocuments
    // (false) leaves documents that still need saving alone.
    match ctx.call("close_all_documents", crate::writes::with_grant(json!({"closeUnsaved": false}), &grant), 110) {
        Err(e) => sidecar_error(e),
        Ok(r) => {
            // ACCOUNT FOR EVERY DOCUMENT STILL OPEN. `CloseAllDocuments(false)`
            // silently leaves anything that needs saving, and the host reports
            // those in `remaining` — a bare list, with no reason attached and no
            // relation to `skipped`. A caller reading a successful response saw
            // documents left behind and nothing saying why. So whatever the host
            // says is still open and is not already in `skipped` gets an entry
            // here, from the host's own after-the-fact view rather than from the
            // server's expectations.
            let named: Vec<String> = skipped
                .iter()
                .filter_map(|s: &Value| s.get("title").and_then(|t| t.as_str()).map(str::to_string))
                .collect();
            // A FLOODED SESSION HAS NO LISTS. The host omits them above the
            // enumeration cap (GetDocuments does not answer on 2144 documents),
            // and `remaining: null` must not read as "nothing was left" — that is
            // the empty-to-confident collapse, in the verb whose whole job is to
            // account for every document still open.
            if r.get("listsOmittedTooManyDocuments").and_then(|v| v.as_bool()).unwrap_or(false) {
                skipped.push(json!({
                    "title": Value::Null,
                    "path": Value::Null,
                    "dirty": Value::Null,
                    "why": format!(
                        "UNKNOWN: {} document(s) were open and {} are open now — too many to \
                         enumerate, so this bridge cannot name which ones are still there. It is \
                         not a claim that none are. swx_docs reports the count and a sample; if \
                         these are leaked import temporaries, swx_close_import_temps drains them.",
                        r.get("documentCountBefore").cloned().unwrap_or(Value::Null),
                        r.get("documentCountAfter").cloned().unwrap_or(Value::Null),
                    ),
                }));
            }
            for d in r.get("remaining").and_then(|v| v.as_array()).cloned().unwrap_or_default() {
                let title = d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();
                if named.iter().any(|n| crate::verbs::same_title(n, &title)) {
                    continue;
                }
                let dirty = d.get("dirty").and_then(|x| x.as_bool());
                skipped.push(json!({
                    "title": title,
                    "path": d.get("path").cloned(),
                    "dirty": dirty,
                    "why": if dirty == Some(true) {
                        "still open: SOLIDWORKS does not close a document with unsaved changes, \
                         and closeUnsaved was not set for it"
                    } else {
                        "still open after CloseAllDocuments — SOLIDWORKS declined to close it \
                         (a document being edited in place, or one held by a modal)"
                    },
                }));
            }
            crate::ledger::record(
                "close_all_documents",
                json!({
                    "closeUnsaved": close_unsaved,
                    "discarded": discarded,
                    "skipped": skipped.len(),
                    "reason": arg_str(args, "reason"),
                }),
            );
            let mut out = ok_merge(r, json!({
                "closeUnsaved": close_unsaved,
                "discarded": discarded,
                "skipped": skipped,
                "_hint": "Unsaved work is discarded ONLY with closeUnsaved:true, which needs \
                          the process-level write opt-in, confirm:true and a reason — the same \
                          bar as any other write to the user's data, and every discarded \
                          document is named in the ledger with that reason. Without it, \
                          documents needing a save are left alone and listed in `skipped`, \
                          which accounts for EVERY document still open when this returns. \
                          Throwing away somebody's unsaved model because a script wanted a \
                          clean slate is not a decision to make casually; swx_reset_workspace \
                          closes only what the bridge created and needs none of this.",
            }));
            if let Value::Object(m) = &mut out {
                m.remove("closedAll");
            }
            out
        }
    }
}

/// `swx_reset_workspace` — close the scratch documents THIS BRIDGE created, and
/// nothing else. The ledger is what makes that possible: a document the bridge
/// did not create is never in it, so it is never closed here.
/// `swx_close_import_temps` — drain leaked STEP/IGES-import temporaries.
///
/// These are documents SOLIDWORKS created under the bridge's own temp directory
/// (`…\Temp\swx<pid>\IC~~\…`) while converting an imported file, and never
/// saved to a real location. A live session was found holding 1342 of them
/// (8 GB) — the same two fixtures imported over and over and never closed, the
/// real root of the crate-build memory exhaustion. See docs/IMPORT-LEAK.md.
///
/// Gated with a `reason` (it closes documents on the user's desktop, and the
/// reason is written for them) but NOT the write opt-in: the IC~~ path is proof
/// the document is a transient bridge temp, stronger than the ownership
/// registry — it survives the pid change that made the 1342 un-ownable, and no
/// user document can match it. So it works in the DEFAULT posture, by design.
///
/// Drains in BOUNDED batches: each `close_import_temps` call closes at most a
/// batch and reports `done`, and this verb loops until done or a wall budget,
/// so neither a single com call nor this verb runs unbounded on a huge count.
pub fn close_import_temps(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    if let Some(g) = require_reason(
        args,
        "swx_close_import_temps",
        "it closes leaked STEP/IGES import temporaries on the user's SOLIDWORKS session",
    ) {
        return g;
    }
    let batch = args.get("batch").and_then(|v| v.as_u64()).unwrap_or(200).clamp(1, 1000);
    let started = std::time::Instant::now();
    let budget = std::time::Duration::from_secs(
        args.get("maxSeconds").and_then(|v| v.as_u64()).unwrap_or(240).clamp(5, SWEEP_MAX_SECS),
    );
    let mut total_closed = 0u64;
    let mut batches = 0u64;
    let mut before: Option<i64> = None;
    let mut after: Option<i64> = None;
    let mut done = false;
    let mut last_err: Option<String> = None;
    // THE ROOT THE DOCUMENTS WERE COMPARED AGAINST, and the documents that carry
    // the launcher's tail but failed that comparison. Without these a matched-
    // nothing drain is indistinguishable from a nothing-to-match drain, which is
    // the whole of adversary pass 8's finding.
    let mut temp_root: Value = Value::Null;
    let mut temp_roots: Value = Value::Null;
    let mut elsewhere: u64 = 0;
    let mut elsewhere_sample: Value = Value::Null;
    let mut scanned: Value = Value::Null;
    let mut walk_timed_out: Value = Value::Null;
    let mut walk_complete: Value = Value::Null;
    let mut walk_stop: Value = Value::Null;
    let mut collisions_over: Value = Value::Null;
    let mut closed_docs: Vec<Value> = Vec::new();
    let mut close_ignored_path = false;
    let mut close_outcome_unknown = false;
    let mut collisions: Vec<Value> = Vec::new();
    let mut close_failed: Vec<Value> = Vec::new();
    let mut wrong_document_closed = false;
    loop {
        match ctx.call("close_import_temps", json!({"max": batch}), 60) {
            Err(e) => {
                last_err = Some(e.message);
                break;
            }
            Ok(r) => {
                // NO RESOLVED %TEMP%, NO DRAIN. The temp root is the whole
                // location proof (a shape test alone qualified a restored profile
                // backup on another volume), so when the host cannot resolve it
                // there is nothing this verb can safely identify — and a
                // `closed:0` success would read as "there was nothing to drain"
                // on a session holding hundreds of them.
                if r.get("tempRoot").map(|v| v.is_null()).unwrap_or(false) {
                    return err_code(
                        "temp_root_unknown",
                        "cannot drain import temporaries: the com host could not resolve this \
                         user's %TEMP%, and the resolved temp directory is the ONLY proof that a \
                         document is a transient import temp rather than a file the user owns",
                        "Nothing was closed and nothing is claimed about the session. The drain \
                         closes documents with no save prompt, so it identifies them by their \
                         location under the resolved %TEMP% — a path that merely LOOKS like a temp \
                         (a restored profile backup, another user's profile) is not enough. \
                         swx_docs still reports the count and a sample; the user can close them in \
                         SOLIDWORKS, and a bridge restart in their own session usually resolves \
                         %TEMP% again.",
                    );
                }
                batches += 1;
                temp_root = r.get("tempRoot").cloned().unwrap_or(Value::Null);
                temp_roots = r.get("tempRoots").cloned().unwrap_or(Value::Null);
                elsewhere = r.get("importTempsElsewhere").and_then(|v| v.as_u64()).unwrap_or(0);
                elsewhere_sample =
                    r.get("importTempsElsewhereSample").cloned().unwrap_or(Value::Null);
                // How far the host's WALK got. On a flooded session this verb
                // looks at a bounded window per call by design — never at the
                // whole session, which is the call that killed the com host.
                scanned = r.get("documentsScanned").cloned().unwrap_or(Value::Null);
                walk_timed_out = r.get("walkTruncatedByTime").cloned().unwrap_or(Value::Null);
                walk_complete = r.get("walkComplete").cloned().unwrap_or(Value::Null);
                walk_stop = r.get("walkStop").cloned().unwrap_or(Value::Null);
                collisions_over = r.get("titleCollisionsCheckedOver").cloned().unwrap_or(Value::Null);
                if r.get("closeIgnoredPath").and_then(|v| v.as_bool()).unwrap_or(false) {
                    close_ignored_path = true;
                }
                // ABSENT IS UNKNOWN, NOT FALSE. `unwrap_or(false)` here turned a
                // field an older com host never sends into a confident "the
                // outcome was determined" — in the one place that decides whether
                // a human has to go and look. The handshake now refuses a version
                // skew outright, and this is the second guard: a reply that
                // reports a close FAILURE while saying nothing about the outcome
                // is treated as undetermined.
                match r.get("closeOutcomeUnknown").and_then(|v| v.as_bool()) {
                    Some(true) => close_outcome_unknown = true,
                    Some(false) => {}
                    None => {
                        if r.get("closeFailed").and_then(|v| v.as_array()).map(|a| !a.is_empty())
                            .unwrap_or(false)
                        {
                            close_outcome_unknown = true;
                        }
                    }
                }
                // EVERY DOCUMENT CLOSED, with its title AND its path — the caller
                // needs to know WHICH files went, not how many.
                if let Some(a) = r.get("closedDocs").and_then(|v| v.as_array()) {
                    closed_docs.extend(a.iter().cloned());
                }
                // A TITLE IS NOT AN IDENTITY: victims whose title another open
                // document shares are never closed, and the caller has to be told
                // WHICH, because the fix is theirs to make (save one of them under
                // another name, or close it themselves).
                if let Some(a) = r.get("titleCollisions").and_then(|v| v.as_array()) {
                    collisions.extend(a.iter().cloned());
                }
                if let Some(a) = r.get("closeFailed").and_then(|v| v.as_array()) {
                    close_failed.extend(a.iter().cloned());
                }
                if r.get("wrongDocumentClosed").and_then(|v| v.as_bool()).unwrap_or(false) {
                    wrong_document_closed = true;
                }
                let closed = r.get("closed").and_then(|v| v.as_u64()).unwrap_or(0);
                total_closed += closed;
                if before.is_none() {
                    before = r.get("documentCountBefore").and_then(|v| v.as_i64());
                }
                after = r.get("documentCountAfter").and_then(|v| v.as_i64());
                done = r.get("done").and_then(|v| v.as_bool()).unwrap_or(true);
                if closed > 0 {
                    crate::ledger::record(
                        "import_temps_closed",
                        json!({"closed": closed, "batch": batches, "by": "swx_close_import_temps"}),
                    );
                }
                // Stop when the host says there is nothing left, when a batch
                // closed nothing (no progress), or when the wall budget is up.
                // STOP IMMEDIATELY on the impossible one. If a close reported
                // success and the document is still open, something else was
                // closed — nothing further is attempted, and the caller is told
                // loudly rather than left to notice.
                if wrong_document_closed
                    || close_ignored_path
                    || close_outcome_unknown
                    || done
                    || closed == 0
                    || started.elapsed() >= budget
                {
                    break;
                }
            }
        }
    }
    if let Some(e) = last_err {
        // Report what the host said, but keep whatever we DID close visible —
        // a partial drain that then hit an error still made progress.
        return err_code(
            "import_temp_drain_failed",
            format!("closed {total_closed} import temp(s), then the com host errored: {e}"),
            "Some import temporaries were closed before the error; run swx_close_import_temps              again to continue, and swx_docs to see the current count.",
        );
    }
    // ── THE SECOND KIND OF IMPORT TEMPORARY, added 0.6.0 after the live run.
    //    The 1342 leaked temps had `…\swx<pid>\IC~~\…` paths, so the drain's whole
    //    definition of "a temporary" was that path shape. A FRESH IMPORT HAS NO
    //    PATH AT ALL (`LoadFile4`, measured 2026-09-11) — so on the machine this
    //    verb exists for, three open import temporaries matched nothing and the
    //    drain reported `closed:0, done:true`: the silent no-op again, from a new
    //    cause.
    //
    //    A document THIS BRIDGE CREATED BY IMPORTING, still unsaved, is a
    //    temporary by ownership rather than by path — and the ownership registry
    //    is the record that says so. As of 0.6.0 every import is given a file at
    //    birth, so this is the path for documents created BEFORE that (or with
    //    keepUnsaved:true), and for anything the identity step could not name.
    let (session, open_docs, _active, list_truncated) = ownership_context(ctx);
    let mut owned_unsaved: Vec<Value> = Vec::new();
    // AN UNLISTABLE SET IS NOT A UNIQUE ONE. With the open documents unlistable
    // this bridge cannot tell whether a title it is about to close by name is
    // shared — so it does not attempt any of them, and `done` cannot be true.
    let mut registry_sweep_blind = false;
    if list_truncated {
        registry_sweep_blind = !ctx.scratch_titles().is_empty();
    }
    if !list_truncated {
        for d in &open_docs {
            let title = doc_title(d);
            let path = doc_path(d).filter(|p| !p.is_empty());
            if path.is_some() {
                continue; // pathed: the location rule above already judged it
            }
            if ctx.owns_doc(&title, None, session) {
                owned_unsaved.push(json!({"title": title, "path": Value::Null}));
            }
        }
    }
    // Close the ones we can still name. A pathless document can only be named by
    // TITLE, so it is closable only while that title is unique among the open
    // documents — which is exactly what `close_doc` enforces, and exactly why
    // 0.6.0 stopped creating pathless documents in the first place.
    let mut owned_closed = 0u64;
    let mut unaddressable: Vec<Value> = Vec::new();
    for d in &owned_unsaved {
        let title = d.get("title").and_then(|v| v.as_str()).unwrap_or("").to_string();
        // CASE-INSENSITIVELY, because that is how `CloseDoc` RESOLVES a name on
        // Windows. Comparing with `==` made the guard blind to an ambiguity the
        // close itself can act on: the bridge's untitled `Part1.SLDPRT` beside
        // the user's saved `part1.SLDPRT` looked unique here and resolved to
        // whichever SOLIDWORKS found first there. A guard must ask the question
        // the way the act will answer it.
        let same = open_docs
            .iter()
            .filter(|o| doc_title(o).eq_ignore_ascii_case(&title))
            .count();
        if !title_resolves_uniquely(&open_docs, &title) {
            unaddressable.push(json!({
                "title": title,
                "openWithThatTitle": same,
                "why": "this bridge created this document and it has NO PATH, so its title is the \
                        only name it has — and another open document shares it, up to the case \
                        SOLIDWORKS ignores when it resolves a name. No API call can say which one \
                        is meant, so nothing was attempted.",
            }));
            continue;
        }
        match ctx.call("close_doc", json!({"docName": title, "save": false}), 60) {
            Ok(_) => {
                owned_closed += 1;
                ctx.forget_scratch_doc(&title, None);
            }
            Err(e) => unaddressable.push(json!({
                "title": title,
                "openWithThatTitle": same,
                "why": format!("the com host would not close it: {}", e.message),
            })),
        }
    }
    let total_closed = total_closed + owned_closed;

    // A DRAIN THAT MATCHED NOTHING WHILE DOCUMENTS CARRY THE LAUNCHER'S TAIL has
    // not finished — it has compared against the wrong directory, and saying
    // "every open import temporary was closed" there is a confident wrong claim
    // about the user's session. On 0.5.3 that is exactly what it said, with three
    // temps open. A miss is the safe direction for the CLOSE; it is never the safe
    // direction for the REPORT.
    let root_mismatch = elsewhere > 0 && total_closed == 0;
    // A CLOSE THAT WENT TO THE WRONG DOCUMENT is the one outcome this verb must
    // never produce, so it is reported as a FAILURE with everything measured —
    // not as a partial success with a note.
    // WE DO NOT KNOW WHAT HAPPENED, and that is the answer. The count could not be
    // read after a close that reported success on a document that is still open —
    // and a count can become unreadable BECAUSE something large just changed, so
    // the reassuring reading ("the call did nothing") is exactly the one this
    // bridge may not take.
    if close_outcome_unknown {
        crate::ledger::record(
            "import_temp_drain_outcome_undetermined",
            json!({"closed": total_closed, "closeFailed": close_failed.clone()}),
        );
        let victim = close_failed
            .last()
            .and_then(|f| f.get("path").and_then(|p| p.as_str()))
            .unwrap_or("(unknown)")
            .to_string();
        let mut out = err_code(
            "close_outcome_unknown",
            format!(
                "UNDETERMINED: CloseDoc returned success for {victim}, that document is still \
                 open, and the open-document count could not be read afterwards — so it cannot be \
                 told whether NOTHING was closed or ANOTHER document was. {total_closed} \
                 temporary/ies had been closed before this point"
            ),
            "A HUMAN MUST LOOK. Ask the user what is on their screen: if a document of theirs has \
             closed, it was NOT saved by this bridge. `closeFailed` names the path this call was \
             about and the count immediately before it. The drain stopped and will not continue \
             until the count reads again — an unreadable count usually means the session is under \
             pressure, and swx_status reports the process's memory.",
        );
        if let Value::Object(m) = &mut out {
            m.insert("outcome".into(), json!("undetermined"));
            m.insert("closeOutcomeUnknown".into(), json!(true));
            m.insert("closed".into(), json!(total_closed));
            m.insert("closedDocs".into(), json!(closed_docs));
            m.insert("closeFailed".into(), json!(close_failed));
            m.insert("titleCollisions".into(), json!(collisions));
            m.insert("documentCountBefore".into(), before.map(Value::from).unwrap_or(Value::Null));
            m.insert("documentCountAfter".into(), after.map(Value::from).unwrap_or(Value::Null));
            m.insert("windowEtiquette".into(), json!("background"));
        }
        return out;
    }
    // THE PREMISE FAILED, NOT THE MACHINE. `CloseDoc`'s help text says only "name
    // of the document to close"; that a FULL PATH is accepted is this bridge's
    // assumption. If the call returns success and changes nothing, that assumption
    // is wrong on this build — and the answer is NOT to fall back to a title,
    // because a title is not an identity and that is the whole reason the close key
    // was changed. Refuse, name the premise, and leave the session alone.
    if close_ignored_path {
        crate::ledger::record(
            "import_temp_drain_close_by_path_unsupported",
            json!({"closed": total_closed, "closeFailed": close_failed.clone()}),
        );
        let mut out = err_code(
            "close_by_path_unsupported",
            format!(
                "STOPPED: CloseDoc(<full path>) returned success, the document is still open, and \
                 the open-document count did not move — this SOLIDWORKS build does not appear to \
                 accept a full path as the close key. {total_closed} temporary/ies had been closed \
                 before this point"
            ),
            "This bridge will NOT retry by title. `CloseDoc` resolves a name, titles are not \
             unique across paths, and a session with leaked temporaries usually holds several \
             documents sharing one title — closing by title would let SOLIDWORKS choose which one \
             died. So nothing further was attempted. Report this with the VERBATIM response (it is \
             the one premise in the drain that has never been proven on a live machine); meanwhile \
             the user can close the temporaries in SOLIDWORKS, and swx_docs reports the count and \
             a sample.",
        );
        if let Value::Object(m) = &mut out {
            m.insert("closed".into(), json!(total_closed));
            m.insert("closedDocs".into(), json!(closed_docs));
            m.insert("closeFailed".into(), json!(close_failed));
            m.insert("closeIgnoredPath".into(), json!(true));
            m.insert("titleCollisions".into(), json!(collisions));
            m.insert("documentCountBefore".into(), before.map(Value::from).unwrap_or(Value::Null));
            m.insert("documentCountAfter".into(), after.map(Value::from).unwrap_or(Value::Null));
            m.insert("windowEtiquette".into(), json!("background"));
        }
        return out;
    }
    if wrong_document_closed {
        crate::ledger::record(
            "import_temp_drain_wrong_document",
            json!({"closed": total_closed, "closeFailed": close_failed.clone()}),
        );
        let mut out = err_code(
            "wrong_document_closed",
            format!(
                "STOPPED: a close reported success and the document it was asked to close is \
                 STILL OPEN, which means a DIFFERENT document may have been closed instead. \
                 {total_closed} temporary/ies had been closed before this point"
            ),
            "This should be impossible — the drain closes by full path and refuses any victim \
             whose title another open document shares — so it is a bug to report, with the \
             VERBATIM response. Nothing further was attempted. Tell the user which documents were \
             open: if one of theirs has closed, it was NOT saved by this bridge and its unsaved \
             changes are gone; SOLIDWORKS' own File ▸ Open Recent still lists it if it had a \
             saved path.",
        );
        if let Value::Object(m) = &mut out {
            m.insert("closed".into(), json!(total_closed));
            m.insert("closedDocs".into(), json!(closed_docs));
            m.insert("closeFailed".into(), json!(close_failed));
            m.insert("titleCollisions".into(), json!(collisions));
            m.insert("documentCountBefore".into(), before.map(Value::from).unwrap_or(Value::Null));
            m.insert("documentCountAfter".into(), after.map(Value::from).unwrap_or(Value::Null));
            m.insert("windowEtiquette".into(), json!("background"));
        }
        return out;
    }
    ok(json!({
        "closed": total_closed,
        "batches": batches,
        "documentCountBefore": before,
        "documentCountAfter": after,
        // NOT DONE while documents this bridge created are still open and it
        // cannot name them. That is the state the live run was in, and it
        // reported `done:true`.
        "done": done && !root_mismatch && unaddressable.is_empty() && !registry_sweep_blind,
        // The directory the documents' paths had to sit under, so a mismatch is
        // diagnosable from the output on a machine where nobody can attach a
        // debugger.
        "tempRoot": temp_root,
        "tempRoots": temp_roots,
        "importTempsElsewhere": elsewhere,
        "importTempsElsewhereSample": elsewhere_sample,
        "ownedUnsavedImportsClosed": owned_closed,
        // The registry sweep was not even attempted: see `done`.
        "ownedUnsavedSweepBlind": registry_sweep_blind,
        // NAMED, not counted: a caller who has to tell the user "close these
        // yourself" needs the titles.
        "ownedUnsavedImportsUnaddressable": unaddressable,
        "documentsScanned": scanned,
        "walkTruncatedByTime": walk_timed_out,
        // Did the last walk REACH THE END of the session? A `GetNext` that failed
        // mid-walk is not the end of the session, and `done` must not say it was.
        "walkComplete": walk_complete,
        // reachedEnd | hitMax | timedOut | iterationFailed
        "walkStop": walk_stop,
        "closedDocs": closed_docs,
        "titleCollisions": collisions,
        // The collision check only sees the documents the walk PASSED.
        "titleCollisionsCheckedOver": collisions_over,
        "closeFailed": close_failed,
        "windowEtiquette": "background",
        "_hint": if registry_sweep_blind {
            "This bridge owns documents that may still be open, and the open-document set is too \
             large to enumerate — so it cannot tell whether any of their titles is shared, and a \
             title-only document may only be closed by name. Nothing was attempted on them. Drain \
             the pathed temporaries first (run this again), then swx_docs will enumerate and this \
             sweep can see what it is doing."
                .to_string()
        } else if !unaddressable.is_empty() {
            format!(
                "{} document(s) this bridge created are still open and CANNOT BE NAMED: they have \
                 no path, and another open document shares each one's title, so no SOLIDWORKS API \
                 call can say which is meant. Nothing was attempted on them. Run \
                 swx_close_all_safely, which preserves every document to a recovery folder and \
                 verifies it before closing — it needs no name, because it closes everything. This \
                 cannot happen to documents created by 0.6.0 or later, which are given a file of \
                 their own at birth — see `documentIdentity` on swx_import and swx_new_document.",
                unaddressable.len()
            )
        } else if !collisions.is_empty() {
            format!(
                "{} import temporary/ies were NOT closed because another OPEN document carries the \
                 same title, and `CloseDoc` resolves a document by name without prompting — asking \
                 by that title would let SOLIDWORKS choose which one died. `titleCollisions` lists \
                 the title and every open path that shares it. Close it yourself in SOLIDWORKS, or \
                 save the other document under another name, then run this again. Everything else \
                 in the batch WAS closed ({} of them), each one by its full path and verified gone \
                 afterwards. NOTE: collisions were checked among the {} document(s) this call \
                 SCANNED, not across the whole session — a same-titled document outside that window \
                 is not in this list.",
                collisions.len(),
                total_closed,
                collisions_over
            )
        } else if root_mismatch {
            format!(
                "NOTHING WAS CLOSED, and this is not the same as nothing being open: {elsewhere} \
                 open document(s) carry the launcher's own \\swx<pid>\\IC~~\\<file> path but are \
                 NOT under any temp directory this bridge could resolve ({}). The temp root this \
                 process sees may not be the one SOLIDWORKS uses — Adom Bridge can run under a \
                 different profile or a redirected TEMP, and these temps were written by an \
                 earlier bridge process. Nothing was closed BY DESIGN: the resolved temp \
                 directory is the whole proof that a document is transient rather than the user's, \
                 and the close does not prompt. To drain them, find the directory in \
                 `importTempsElsewhereSample` and restart the bridge with SWX_TEMP_ROOT set to the \
                 temp directory above the swx<pid> folder — or close them with \
                 swx_close_all_safely, which preserves every document to recovery first and never \
                 needs anybody to close anything by hand \
                 SOLIDWORKS. swx_docs reports the count.",
                match &temp_roots {
                    Value::Array(a) if !a.is_empty() => a
                        .iter()
                        .filter_map(|v| v.as_str())
                        .collect::<Vec<_>>()
                        .join(", "),
                    _ => "none".to_string(),
                }
            )
        } else if done {
            "Every open STEP/IGES import temporary was closed. These are documents SOLIDWORKS              created under the bridge's temp dir while converting an imported file and never              saved anywhere real — never a user document (a user file has a real saved path).              Import leaves one open unsaved document per call, like swx_new_document; close or              save it to avoid leaking more."
                .to_string()
        } else {
            "Closed a batch of import temporaries and there are more — run swx_close_import_temps              again to continue. It is bounded per call so it cannot wedge the com host on a huge              count. swx_docs shows the current count."
                .to_string()
        },
    }))
}

/// THE LAUNCH'S CLAIM MUST BE ABOUT THE SESSION THIS VERB WOULD ACT ON.
///
/// Shared by every verb that can END or EMPTY somebody's SOLIDWORKS. An
/// attribution record is a claim about ONE process; if COM is now answering for a
/// different one, that claim authorises nothing here. When no claim exists this
/// returns `Ok` and the ordinary rules apply unchanged — a user's own session is
/// still refused by the rules that already refuse it.
///
/// WIRED INTO THREE VERBS, and that is the point: `identity_matches_attribution`
/// shipped in 0.6.7 unit-tested and with ZERO production callers — the fifth
/// guard in this project to do so, and the third about attribution. A guard that
/// nothing calls is a comment with a test attached.
fn identity_claim_ok(ctx: &Ctx) -> Result<(), Value> {
    let claimed = crate::launch::attributed_solidworks();
    if claimed.is_none() {
        return Ok(());
    }
    // The live identity, from COM and the census — the same pair the quit ladder
    // builds its `Identity` from.
    let state = ctx.call("app_state", json!({}), 20);
    let pid = state
        .as_ref()
        .ok()
        .and_then(|s| s.get("processId"))
        .and_then(|v| v.as_u64())
        .map(|p| p as u32);
    let os = crate::launchos::os();
    let identity = pid.and_then(|pid| {
        let created = os
            .processes()
            .ok()?
            .iter()
            .find(|p| p.pid == pid)
            .and_then(|p| p.created_at)?;
        Some(crate::quit::Identity { pid, created_at: created, main_hwnd: None })
    });
    match crate::quit::identity_matches_attribution(claimed, identity.as_ref()) {
        Ok(()) => Ok(()),
        Err(r) => Err(err_code(
            r.code(),
            r.message(),
            "The launch record and the live COM session name DIFFERENT SOLIDWORKS processes, so \
             this bridge cannot say the session it would act on is the one it launched. Nothing \
             was attempted. swx_status reports both; swx_start re-establishes which session it is \
             talking to, and restarting the bridge clears a stale claim.",
        )),
    }
}

pub fn reset_workspace(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    if let Err(e) = identity_claim_ok(ctx) {
        return e;
    }
    // PRUNE FIRST. Ownership is scoped to a SOLIDWORKS session and to documents
    // that are still open; without this the reported list (and the sweep) could
    // name a title from a previous session — which, since SOLIDWORKS restarts
    // its Part numbering, might now be the USER's document.
    let (session, open_docs, active, truncated) = ownership_context(ctx);
    prune_unless_truncated(ctx, session, &open_docs, truncated);
    // A SWEEP THAT CANNOT SEE THE SESSION IS NOT A CLEAN SWEEP. With the open
    // list unlistable this reported `success:true, closed:[], remaining:[]` while
    // `trackedScratchDocuments` listed rows of its own — a silent no-op that a
    // caller reads as "there was nothing to clean". Refuse the same way
    // `resolve_document` does.
    if truncated {
        let mut out = err_code(
            "document_list_unavailable",
            "swx_reset_workspace cannot sweep: SOLIDWORKS has more documents open than can be \
             enumerated (or would not say how many), so the open-document list is UNKNOWN — not \
             empty, and nothing can be identified as ours to close",
            "This is almost always leaked STEP/IGES import temporaries: run \
             swx_close_import_temps to drain them (it identifies them by path and needs no write \
             opt-in), then retry. NOTHING was closed. The ownership registry is intact — it is \
             deliberately not pruned against an unlistable session — so retrying after the drain \
             sweeps exactly what it should.",
        );
        if let Value::Object(m) = &mut out {
            // What the bridge BELIEVES it owns is still worth reporting: it is
            // the thing a caller would have swept, and seeing it is how they can
            // tell "nothing to do" from "I could not look".
            m.insert("trackedScratchDocuments".into(), json!(ctx.scratch_titles()));
            m.insert("closed".into(), json!([]));
            m.insert("remaining".into(), Value::Null);
            m.insert("solidworksSession".into(), json!(session));
        }
        return out;
    }

    let only: Option<Vec<String>> = args.get("titles").and_then(|v| v.as_array()).map(|a| {
        a.iter().filter_map(|v| v.as_str()).map(String::from).collect()
    });
    let force = arg_bool(args, "force", false);
    // THE USER IS PRESENT (0.10.9): closing the bridge's OWN never-saved scratch
    // restores their screen and never touches their documents, so it goes on —
    // for every one EXCEPT the active document, which they may have clicked
    // into. MEASURED 2026-10-14: refusing the whole sweep while new scratch
    // parts were still allowed left 18 of the bridge's windows on Drew's screen.
    // `_presenceMeasuredByCaller` is the quit ladder's prime, which measured
    // presence itself before it made its one part (underscore keys never come
    // from a caller: the dispatcher strips them).
    let present: Option<Value> = if crate::presence::overridden(args)
        || args.get("_presenceMeasuredByCaller").and_then(|v| v.as_bool()) == Some(true)
    {
        None
    } else {
        crate::presence::measure(ctx).filter(|(p, _)| *p).map(|(_, e)| e)
    };
    let mut left_active: Vec<String> = Vec::new();
    let mut qualifying = 0usize;
    let mut closed = Vec::new();
    let mut skipped = Vec::new();
    for doc in &open_docs {
        let title = doc.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();
        let path = doc.get("path").and_then(|p| p.as_str()).filter(|s| !s.is_empty());
        let saved = path.is_some();
        // owns_doc, not owns: once a document has a path THAT is its identity,
        // and a different file that merely shares its title is not ours.
        if !ctx.owns_doc(&title, path, session) {
            skipped.push(json!({"title": title, "why": "not created by this bridge in this SOLIDWORKS session"}));
            continue;
        }
        if present.is_some() && active.as_deref() == Some(title.as_str()) {
            left_active.push(title.clone());
            skipped.push(json!({"title": title, "why": "ours, but it is the ACTIVE document and the user is present — they may have clicked into it"}));
            continue;
        }
        if let Some(want) = &only {
            if !want.contains(&title) {
                skipped.push(json!({"title": title, "why": "ours, but not named in titles[]"}));
                continue;
            }
        }
        // A FILE THE BRIDGE GAVE IT IS NOT A SAVE THE USER ASKED FOR. From
        // 0.6.0 every bridge-created document has a file (that is what makes it
        // addressable); one inside the bridge's own scratch directory is still
        // scratch, and the sweep exists to close exactly those.
        let saved = saved
            && !path.map(crate::scratch::is_bridge_scratch_path).unwrap_or(false);
        if saved && !force {
            // A PATH IS NOT A SAVE. `saved = path.is_some()` conflates the two,
            // and an import temporary has a path (`…\Temp\swx<pid>\IC~~\…`)
            // while never having been saved anywhere the user could find it.
            // Telling a caller it "has been saved to disk; pass force:true"
            // invites a `force` sweep to prove a fact that was never true; the
            // honest route for a temp is the verb that exists for temps.
            let is_temp = path.map(crate::comhost::host::is_import_temp).unwrap_or(false);
            skipped.push(json!({
                "title": title,
                "path": path,
                "importTemp": is_temp,
                "why": if is_temp {
                    "this is a STEP/IGES import TEMPORARY — it has a path under the bridge's temp \
                     directory but was never saved anywhere real. swx_close_import_temps drains \
                     these (no write opt-in needed); `force` is not the right tool."
                } else {
                    "has been saved to disk; pass force:true"
                },
            }));
            continue;
        }
        // A PATHLESS DOCUMENT IS CLOSED BY ITS TITLE, and `CloseDoc` resolves a
        // title without regard to case — so the sweep asks the same question the
        // drain does, in the other verb that reaches this act. Our untitled
        // `Part1.SLDPRT` beside the user's saved `part1.SLDPRT` is one name to
        // SOLIDWORKS, and it would choose.
        if path.is_none() && !title_resolves_uniquely(&open_docs, &title) {
            skipped.push(json!({
                "title": title,
                "path": Value::Null,
                "why": "ours, but it has NO PATH and another open document answers to the same \
                        title (SOLIDWORKS resolves a title without regard to case) — so no API \
                        call can say which one is meant and nothing was attempted. Close it in \
                        SOLIDWORKS, or save the other document under another name.",
            }));
            continue;
        }
        qualifying += 1;
        match ctx.call("close_doc", json!({"docName": title.clone(), "path": path, "save": false}), 60) {
            Ok(_) => {
                crate::ledger::record(
                    "document_closed",
                    json!({"title": title, "by": "reset_workspace", "userPresent": present.is_some()}),
                );
                ctx.forget_scratch_doc(&title, path.as_deref());
                closed.push(title);
            }
            Err(e) => skipped.push(json!({
                "title": title,
                "why": e.message,
                "detail": e.detail,
                "hostError": e.data.clone().unwrap_or(Value::Null),
            })),
        }
    }
    // NULL, NOT `[]`, when the read failed or came back truncated. An empty
    // array here reads as "nothing is left open", which is the one thing a
    // failed read must never be allowed to say.
    if let Some(evidence) = &present {
        if qualifying == 0 {
            let mut out = err_code(
                "user_is_active",
                format!(
                    "swx_reset_workspace: the user is working in this SOLIDWORKS ({}), and no document qualifies — \
                     the only scratch document of the bridge's is the active one{}",
                    evidence.get("basis").and_then(|b| b.as_str()).unwrap_or("present"),
                    if left_active.is_empty() { " (and there is none)" } else { "" }
                ),
                "Nothing was closed. While the user is present, the bridge closes its own never-saved scratch \
                 documents EXCEPT the active one; it never touches their documents.",
            );
            if let Value::Object(m) = &mut out {
                m.insert("userPresence".into(), evidence.clone());
                m.insert("leftActive".into(), json!(left_active));
                m.insert("closed".into(), json!([]));
            }
            crate::ledger::record("user_present_refusal", json!({"verb": "swx_reset_workspace", "leftActive": left_active}));
            return out;
        }
    }
    let remaining = match ctx.call("docs", json!({}), 30) {
        Ok(r) if r.get("truncated").and_then(|v| v.as_bool()) != Some(true) => {
            r.get("docs").cloned().unwrap_or(Value::Null)
        }
        _ => Value::Null,
    };
    ok(json!({
        "leftActive": left_active,
        "userPresent": present.is_some(),
        "closed": closed,
        "untitledScratchClosed": closed.len(),
        "skipped": skipped,
        "remaining": remaining,
        "force": force,
        "solidworksSession": session,
        "trackedScratchDocuments": ctx.scratch_titles(),
        "_hint": "Closes ONLY documents this bridge created IN THIS SOLIDWORKS SESSION and has \
                  not saved. Ownership is scoped to the session because SOLIDWORKS restarts its \
                  Part1/Assem1 numbering every time it starts — a title remembered across a \
                  restart would eventually match a document the USER made. Entries are also \
                  dropped when the document stops being open at all, which covers a close in \
                  the SOLIDWORKS UI, a crash, and CloseAllDocuments. `skipped` says why each \
                  survivor survived; titles:[...] to be surgical, force:true to include scratch \
                  documents that have since been saved. swx_close_all_documents is the bigger \
                  hammer, and it asks first.",
    }))
}

/// Should `swx_kill` refuse on unsaved-work grounds?
///
/// The subtlety, and the round-7 defect: when COM is wedged the probe FAILS, and
/// the old code turned that into `unsaved = 0` and skipped the guard — in
/// exactly the situation the verb exists for. An unanswerable question is not
/// permission, so unknown counts as unsafe.
pub(crate) fn kill_is_unsafe(unsaved_known: bool, unsaved: u64, force: bool) -> bool {
    if force {
        return false;
    }
    !unsaved_known || unsaved > 0
}

/// `swx_kill` — taskkill SOLIDWORKS. Gated twice, and it says why you almost
/// certainly should not.
pub fn kill(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    if let Some(g) = require_reason(args, "swx_kill", "force-killing a CAD application discards every unsaved document in it") {
        return g;
    }
    if let Some(g) = require_confirm(args, "swx_kill", "FORCE-KILL SOLIDWORKS and discard every unsaved document") {
        return g;
    }
    // THE CHOKE POINT. This verb ends the user's SOLIDWORKS session — the same
    // harm `swx_stop` does, more abruptly — but it goes out through ab rather
    // than the com host, so neither of the two vetoes was ever in its path. Its
    // own gates were `confirm` and `reason`, both of which a caller writes for
    // itself. `Kind::EndSession` is gated unconditionally, so the process-level
    // opt-in is required here exactly as it is for stop.
    let grant = match crate::writes::authorize_in(
        ctx,
        args,
        "swx_kill",
        crate::writes::Kind::EndSession,
        None,
        None,
    ) {
        Ok(g) => g,
        Err(e) => return e,
    };
    // If COM is wedged the probe fails — which is EXACTLY the case this verb
    // exists for, so "we could not count the unsaved documents" must mean
    // "assume there are some", not "assume zero". The old code let an
    // unanswerable question skip the guard entirely.
    let state = ctx.call("app_state", json!({}), 20);
    let unsaved_known = state.is_ok();
    let unsaved = state
        .ok()
        .and_then(|s| s.get("unsavedDocuments").and_then(|v| v.as_u64()))
        .unwrap_or(0);
    if kill_is_unsafe(unsaved_known, unsaved, arg_bool(args, "force", false)) {
        return err_code(
            "unsaved_documents",
            if unsaved_known {
                format!("SOLIDWORKS has {unsaved} document(s) with unsaved changes")
            } else {
                "SOLIDWORKS would not answer, so this bridge cannot tell what is unsaved"
                    .to_string()
            },
            "Killing now throws that work away. Save them (swx_save), close them, or pass \
             force:true if the user has explicitly said the changes are disposable. When the \
             application is too wedged to say what it has open — the very situation this verb \
             exists for — that counts as unsafe, not as safe: an unanswerable question is not \
             permission.",
        );
    }
    let reason = arg_str(args, "reason").unwrap_or("").to_string();
    let r = crate::caller::ad_callback(
        "shell_execute",
        json!({
            "command": "taskkill /F /IM SLDWORKS.exe",
            "timeoutSeconds": 60,
            "reason": format!("SOLIDWORKS bridge: {reason}"),
        }),
        Duration::from_secs(90),
    );
    crate::ledger::record(
        "solidworks_killed",
        json!({"reason": reason, "unsavedAtKill": unsaved, "unsavedKnown": unsaved_known, "authorized": grant.user_write}),
    );
    let killed = r.get("success").and_then(|v| v.as_bool()).unwrap_or(false)
        && r.get("exitCode").and_then(|v| v.as_i64()).unwrap_or(0) == 0;
    json!({
        "success": killed,
        "ab": r,
        "unsavedAtKill": if unsaved_known { json!(unsaved) } else { Value::Null },
        "unsavedKnown": unsaved_known,
        "_hint": "The desperate path, for a SOLIDWORKS that swx_stop cannot close (a wedged \
                  process, a modal nobody can answer). It ends the user's session, so it needs \
                  the same process-level opt-in as swx_stop, and it is delegated to ab's \
                  shell_execute so the kill appears in their Activity Log with your reason \
                  attached. swx_stop asks the application to quit properly and is what you want \
                  almost every time.",
        "_reportIssues": crate::verbs::report_issues(),
    })
}

#[cfg(test)]
mod kill_tests {
    use super::kill_is_unsafe;

    #[test]
    fn an_unanswerable_unsaved_probe_counts_as_unsafe() {
        // Known-clean is the only safe answer without `force`.
        assert!(!kill_is_unsafe(true, 0, false), "known clean => safe");
        assert!(kill_is_unsafe(true, 3, false), "known dirty => unsafe");
        // THE fix: a wedged SOLIDWORKS cannot say what it has open, and the old
        // code read that failure as zero and waved the kill through — precisely
        // when the verb is most likely to be used.
        assert!(kill_is_unsafe(false, 0, false), "unknown => unsafe, not zero");
        // `force` is the caller saying the work is disposable, which is theirs
        // to say — but it is on TOP of the process-level opt-in, not instead.
        assert!(!kill_is_unsafe(false, 0, true));
        assert!(!kill_is_unsafe(true, 9, true));
    }
}

// ── windows & dialogs (delegated to ab) ─────────────────────────────────────

/// `swx_window_info` — SOLIDWORKS' windows, as ab sees them.
pub fn window_info(ctx: &Ctx, _args: &Value) -> Value {
    gate!(ctx);
    let (hwnd, pid) = window_identity(ctx);
    match crate::abwin::list_windows(AB_TIMEOUT) {
        Err(e) => ok(json!({
            // NOT an empty array. "ab could not be asked" and "SOLIDWORKS has no
            // windows" are completely different facts, and an empty list reads
            // as the second — in the live run it did exactly that.
            "windows": Value::Null,
            "windowCount": Value::Null,
            "abAvailable": false,
            "mainHwnd": hwnd,
            "abError": e,
            "_hint": "THE WINDOW LIST IS UNKNOWN, not empty — ab could not be asked. Do not read \
                      this as \"SOLIDWORKS has no windows\". The usual cause is a bridge that was \
                      launched by hand rather than spawned by Adom Bridge, so it never inherited \
                      ADOM_DIRECT_API_URL; the other is a disconnected desktop relay. The COM \
                      side still answered: `mainHwnd` is SOLIDWORKS' own window handle, and every \
                      COM-backed verb works normally.",
        })),
        Ok(all) => {
            let ours = crate::abwin::solidworks_windows(&all, hwnd, pid);
            ok(json!({
                "dialogCensus": judged_dialog_census_without_com(),
                "mainHwnd": hwnd,
                "abAvailable": true,
                "windowCount": ours.len(),
                "windows": ours,
                "desktopWindowCount": all.len(),
                "windowEtiquette": "background",
                "_hint": "Read-only; nothing is raised, moved or focused. `ownerConfirmed:false` \
                          means the window is a plain Win32 dialog that COULD belong to another \
                          app — ab's flat window list carries no owner pid, so this bridge \
                          reports the doubt instead of hiding it. swx_screenshot on mainHwnd \
                          captures the app AND every dialog it really owns.",
            }))
        }
    }
}

/// THE BRIDGE'S OWN VERDICT on every dialog of the running SOLIDWORKS —
/// blocking or notification, with the evidence — taken WITHOUT COM (the process
/// census names the one SOLIDWORKS), so it is available during startup and to a
/// gate that must not act on a window it has merely seen (0.9.6, G2).
fn judged_dialog_census_without_com() -> Value {
    let os = crate::launchos::os();
    let census = match os.processes() {
        Ok(c) => c,
        Err(e) => return json!({"readable": false, "why": format!("no process census: {e}")}),
    };
    let sw: Vec<&crate::launchos::ProcInfo> =
        census.iter().filter(|p| p.image.eq_ignore_ascii_case("SLDWORKS.exe")).collect();
    let (pid, created) = match sw.as_slice() {
        [] => return json!({"readable": true, "solidworksRunning": false, "blocking": Value::Null, "notifications": []}),
        [one] => match one.created_at {
            Some(c) => (one.pid, c),
            None => return json!({"readable": false, "why": "the SOLIDWORKS process's creation time could not be read"}),
        },
        many => {
            return json!({"readable": false,
                          "why": format!("{} SOLIDWORKS processes are running; the census cannot say which is meant", many.len())})
        }
    };
    match dialog_census_of(os.as_ref(), &census, pid, created) {
        None => json!({"readable": false, "processId": pid,
                       "why": "the windows of SOLIDWORKS or its children could not be read"}),
        Some(c) => json!({
            "readable": true,
            "processId": pid,
            "blocking": c.blocking.map(|d| d.to_json()),
            "notifications": c.notifications.iter().map(|d| d.to_json()).collect::<Vec<_>>(),
        }),
    }
}

/// `swx_dismiss_blocking_dialogs` — REPORT by default; closing is opt-in.
///
/// Learned live, the hard way, on 2026-09-02: closing a stuck SOLIDWORKS modal
/// through UIA took the whole application down with it. So the default here is
/// to describe what is on screen, and actually closing anything needs
/// `confirm:true` plus a `reason` — and a "Save changes?" dialog is refused
/// outright, because answering a question about the user's unsaved work is not
/// a decision a bridge gets to make.
pub fn dismiss_blocking_dialogs(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let close = arg_bool(args, "close", false);
    // The gates come FIRST, before any work: a caller who forgot the reason gets
    // the same answer whether or not ab happens to be reachable right now.
    if close {
        if let Some(g) = require_reason(args, "swx_dismiss_blocking_dialogs", "closing a dialog on someone else's live CAD session is a visible act") {
            return g;
        }
        if let Some(g) = require_confirm(args, "swx_dismiss_blocking_dialogs", "close SOLIDWORKS dialogs, which can take the application down with them") {
            return g;
        }
    }
    // THE NATIVE CENSUS FIRST (0.11.1): SOLIDWORKS' own windows, judged, with
    // where each one came from. The ONE dialog this bridge may cancel is in it:
    // the standard file dialog with an EMPTY file name that a bridge verb
    // provably raised (dialogs::may_cancel) — Cancel is a no-op, and it is the
    // bridge's own mess. Everything else is the user's to answer.
    let os = crate::launchos::os();
    let native: Option<Vec<crate::quit::BlockingDialog>> = crate::dialogs::census_windows(os.as_ref()).map(|(rows, _)| {
        let c = crate::quit::dialog_census(&rows, os.as_ref());
        let mut all: Vec<crate::quit::BlockingDialog> = c.blocking.into_iter().chain(c.more_blocking).collect();
        all.extend(c.notifications);
        all
    });
    let only_hwnds: Vec<i64> = args.get("hwnds").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|v| v.as_i64()).collect()).unwrap_or_default();
    let mut cancelled: Vec<Value> = Vec::new();
    let mut cancel_refused: Vec<Value> = Vec::new();
    if close {
        let reason = arg_str(args, "reason").unwrap_or("").to_string();
        for d in native.iter().flatten() {
            if !only_hwnds.is_empty() && !only_hwnds.contains(&d.hwnd) {
                continue;
            }
            match crate::dialogs::may_cancel(d.hwnd, os.as_ref()) {
                Ok((verb, sighting)) => {
                    let r = os.cancel_dialog(d.hwnd);
                    let attribution = crate::dialogs::attribution(d.hwnd, os.as_ref());
                    crate::ledger::record(
                        "dialog_cancelled",
                        json!({"hwnd": d.hwnd, "title": d.title, "raisedDuring": verb, "sighting": sighting,
                               "ok": r.is_ok(), "error": r.as_ref().err(), "reason": reason}),
                    );
                    cancelled.push(json!({"hwnd": d.hwnd, "title": d.title, "ok": r.is_ok(), "error": r.err(),
                                          "raisedDuring": verb, "attribution": attribution,
                                          "how": "WM_COMMAND IDCANCEL — what Esc or its Cancel button does; nothing is opened or changed"}));
                }
                Err(why) => cancel_refused.push(json!({"hwnd": d.hwnd, "title": d.title, "why": why})),
            }
        }
    }
    let native_report: Option<Vec<Value>> = crate::dialogs::census_windows(os.as_ref()).map(|(rows, _)| {
        let c = crate::quit::dialog_census(&rows, os.as_ref());
        c.all_blocking().iter().map(|d| d.report(os.as_ref())).chain(c.notifications.iter().map(|d| d.report(os.as_ref()))).collect()
    });
    let (hwnd, pid) = window_identity(ctx);
    let all = match crate::abwin::list_windows(AB_TIMEOUT) {
        Err(e) if native_report.is_some() => {
            return ok(json!({
                "dialogs": native_report,
                "dialogsKnown": true,
                "censusSource": "native",
                "cancelled": cancelled,
                "cancelRefused": cancel_refused,
                "closed": [],
                "abError": e,
                "_hint": "The native census (SOLIDWORKS' own windows) is complete; ab could not be \
                          reached, so nothing beyond a bridge-raised empty file dialog could be closed.",
            }))
        }
        Err(e) => {
            return ok(json!({
                // NULL, not []. An empty census reads as "nothing is in the
                // way" — which is precisely the wrong thing to tell a caller
                // about to drive a SOLIDWORKS that may be sitting on a modal.
                "dialogs": Value::Null,
                "dialogsKnown": false,
                "abError": e,
                "_hint": "ab could not enumerate windows, so no dialog census is possible right \
                          now. swx_status still reports whether SOLIDWORKS is COM-attachable, \
                          which is the other half of 'is it wedged'.",
            }))
        }
        Ok(w) => w,
    };
    let dialogs = crate::abwin::dialog_census(&all, hwnd, pid);
    // A dialog cancelled above is gone; the ab list was read after it.
    let dialogs: Vec<Value> = dialogs.into_iter().filter(|d| !cancelled.iter().any(|c| c["hwnd"] == d["hwnd"] && c["ok"] == true)).collect();
    if !close {
        return ok(json!({
            "dialogs": dialogs,
            "dialogCount": dialogs.len(),
            "nativeDialogs": native_report,
            "closed": [],
            "mainHwnd": hwnd,
            "_hint": "REPORT ONLY. Pass close:true (plus confirm:true and a reason) to close \
                      specific dialogs. Why the default is to look and not touch: on 2026-09-02 \
                      closing a stuck SOLIDWORKS modal through the window manager took the whole \
                      application down with it. A dialog flagged neverAutoAnswer:true \
                      (Save changes?, Document Recovery, licence) is NEVER closed by this verb \
                      at all — that question belongs to the user.",
        }));
    }
    let reason = arg_str(args, "reason").unwrap_or("dismissing a blocking dialog").to_string();
    let only: Vec<i64> = args
        .get("hwnds")
        .and_then(|v| v.as_array())
        .map(|a| a.iter().filter_map(|v| v.as_i64()).collect())
        .unwrap_or_default();
    let mut closed = Vec::new();
    let mut refused = Vec::new();
    for d in &dialogs {
        let h = d.get("hwnd").and_then(|v| v.as_i64()).unwrap_or(0);
        if h == 0 {
            continue;
        }
        if !only.is_empty() && !only.contains(&h) {
            continue;
        }
        if d.get("neverAutoAnswer").and_then(|v| v.as_bool()).unwrap_or(true) {
            refused.push(json!({"hwnd": h, "title": d.get("title"), "why": "this dialog asks about the user's own work"}));
            continue;
        }
        // A frame is never a dialog, whatever its title says. (`dialog_census`
        // only emits `#32770` windows, so this is belt-and-braces rather than
        // the load-bearing check — `is_frame_window` is that. It is kept
        // because the census shape is not this verb's to guarantee.)
        if d.get("isFrame") == Some(&Value::Bool(true)) || d.get("isDialog") == Some(&Value::Bool(false)) {
            refused.push(json!({"hwnd": h, "title": d.get("title"), "why": "this is an application window, not a dialog — swx_stop quits SOLIDWORKS"}));
            continue;
        }
        // EVERY close needs the process-level opt-in — not just the ones for
        // dialogs the bridge did not raise.
        //
        // The narrower rule (allow our own export sheets, gate the rest) was
        // tempting and wrong. In this container ab reports no pid so nothing was
        // attributable and this path never ran; on the real machine ab DOES
        // report pid, `ownerConfirmed` flips true, and dialogs become closable —
        // including ones the user is part-way through answering. And the
        // sharpest evidence is first-hand: on 2026-09-02 closing a stuck
        // SOLIDWORKS modal through the window manager TOOK THE WHOLE
        // APPLICATION DOWN. A verb that can do that should not act on
        // `confirm` + `reason`, both of which a caller writes for itself.
        //
        // `bridgeCaused` is still REPORTED, so a caller can see which dialogs
        // are the bridge's own mess — it just does not license closing them.
        let kind = d.get("kind").and_then(|k| k.as_str()).unwrap_or("unknown");
        if let Err(e) = crate::writes::authorize_in(
            ctx,
            args,
            &format!("swx_dismiss_blocking_dialogs (a '{kind}' dialog)"),
            crate::writes::Kind::EndSession,
            None,
            None,
        ) {
            refused.push(json!({
                "hwnd": h,
                "title": d.get("title"),
                "kind": kind,
                "bridgeCaused": crate::abwin::BRIDGE_CAUSED_DIALOGS.contains(&kind),
                "why": "closing a dialog can take SOLIDWORKS down with it (observed live), so it \
                        needs the process-level write opt-in",
                "refusal": e,
            }));
            continue;
        }
        // ATTRIBUTION. ab's window list carries no owner pid, so a bare Win32
        // dialog class is a CANDIDATE, not a SOLIDWORKS dialog — it is just as
        // likely to be a Save dialog belonging to Word. Closing one on a guess
        // is unacceptable, so an unattributed window is never closed.
        if !d.get("ownerConfirmed").and_then(|v| v.as_bool()).unwrap_or(false) {
            refused.push(json!({
                "hwnd": h,
                "title": d.get("title"),
                "why": "cannot prove this window belongs to SOLIDWORKS (ab's window list carries \
                        no owner pid); it may belong to another application",
            }));
            continue;
        }
        // READ ITS WORDS, with the classifier judge_window uses (adversary pass
        // 44, F6): a save question in a window titled "Warning" is a save
        // question. A window whose words cannot be read is not closed.
        let title = d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();
        let words = match crate::launchos::os().dialog_contents(h) {
            Ok(c) => c,
            Err(e) => {
                refused.push(json!({"hwnd": h, "title": title,
                                    "why": format!("its words could not be read ({e}), so whether it asks about the user's work is unknown")}));
                continue;
            }
        };
        let (wkind, wsource, _, wnever) = crate::quit::classify_words(&title, Some(&words));
        if wnever {
            refused.push(json!({"hwnd": h, "title": title, "kind": wkind,
                                "why": format!("by its {wsource}, this dialog is a '{wkind}' question about the user's own work")}));
            continue;
        }
        let r = crate::abwin::close_window(h, &reason, AB_TIMEOUT);
        let ok_flag = r.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
        crate::ledger::record("dialog_closed", json!({"hwnd": h, "title": d.get("title"), "ok": ok_flag, "reason": reason}));
        closed.push(json!({"hwnd": h, "title": d.get("title"), "ok": ok_flag, "ab": r}));
    }
    ok(json!({
        "dialogs": dialogs,
        "nativeDialogs": native_report,
        "cancelled": cancelled,
        "cancelRefused": cancel_refused,
        "closed": closed,
        "refused": refused,
        "_hint": "Only dialogs PROVABLY owned by SOLIDWORKS are closed (`ownerConfirmed:true`). \
                  ab's window list has no owner pid, so a bare Win32 dialog class is a candidate, \
                  not a fact — it could belong to any application, and this verb will not guess. \
                  To identify one for certain, swx_screenshot on `mainHwnd` captures SOLIDWORKS \
                  AND every popup it really owns. Every close is recorded in swx_action_ledger. \
                  If SOLIDWORKS stops responding afterwards, that is the known hazard: a modal \
                  the application was blocked on can take it down with it.",
    }))
}

/// `swx_close_window` — close ONE window by hwnd.
pub fn close_window(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let hwnd = match args.get("hwnd").and_then(|v| v.as_i64()) {
        Some(h) if h != 0 => h,
        _ => {
            return err(
                "hwnd is required",
                "swx_window_info and swx_dismiss_blocking_dialogs both report the hwnds.",
            )
        }
    };
    if let Some(g) = require_reason(args, "swx_close_window", "closing a window on the user's live desktop is a visible act") {
        return g;
    }
    if let Some(g) = require_confirm(args, "swx_close_window", "close a window on the user's desktop") {
        return g;
    }
    // This verb exists to close a SOLIDWORKS dialog. It is not a general
    // window killer, and it must not become one by accident: the hwnd has to be
    // one we can attribute to SOLIDWORKS.
    let (main, pid) = window_identity(ctx);
    let listed = crate::abwin::list_windows(AB_TIMEOUT)
        .map(|all| crate::abwin::solidworks_windows(&all, main, pid));
    let ab_error = listed.as_ref().err().cloned();
    let attributed = listed
        .unwrap_or_default()
        .into_iter()
        .find(|w| w.get("hwnd").and_then(|h| h.as_i64()) == Some(hwnd));
    // The COM-reported main window is refused WITHOUT consulting ab at all: if
    // ab is unreachable we would otherwise fall through to
    // `window_not_attributed`, which is the right answer for the wrong reason
    // and would flip to allowed the moment ab could see the window.
    if main.is_some() && main == Some(hwnd) {
        return json!({
            "success": false,
            "errorCode": "window_is_application_frame",
            "error": format!("hwnd {hwnd} is SOLIDWORKS' main application window, not a dialog"),
            "mainHwnd": main,
            "_hint": "This verb closes DIALOGS. Closing the application window quits SOLIDWORKS — \
                      raising a Save-changes modal for every unsaved document on the way out, \
                      and on a 3DEXPERIENCE/Connected edition this bridge cannot start it again. \
                      swx_stop asks it to quit properly and is gated for that reason.",
            "_reportIssues": crate::verbs::report_issues(),
        });
    }
    // A FRAME is not a dialog. Closing SOLIDWORKS' main window quits the
    // application — with Save-changes modals on the way out, and on a Connected
    // edition this bridge cannot start it again. It is also the MOST
    // attributable window there is, so the ownership check below passes on it;
    // that is precisely why this refusal has to come first.
    if attributed.as_ref().map(|w| w.get("isFrame") == Some(&Value::Bool(true))).unwrap_or(false) {
        return json!({
            "success": false,
            "errorCode": "window_is_application_frame",
            "error": format!("hwnd {hwnd} is a SOLIDWORKS application window, not a dialog"),
            "window": attributed,
            "mainHwnd": main,
            "_hint": "This verb closes DIALOGS. Closing an application frame quits SOLIDWORKS — \
                      raising a Save-changes modal for every unsaved document on the way out, \
                      and on a 3DEXPERIENCE/Connected edition this bridge cannot start it again. \
                      If the user genuinely wants SOLIDWORKS closed, swx_stop asks it to quit \
                      properly and is gated for that reason. swx_window_info marks every window \
                      with `isFrame` and `closable`.",
            "_reportIssues": crate::verbs::report_issues(),
        });
    }
    let confirmed = attributed
        .as_ref()
        .and_then(|w| w.get("ownerConfirmed").and_then(|v| v.as_bool()))
        .unwrap_or(false);
    if !confirmed {
        return json!({
            "success": false,
            // The refusal is the same either way — but WHY it was refused is
            // not, and a caller that reads "not attributable" when the truth is
            // "ab could not be asked" will go looking for the wrong problem.
            "errorCode": if ab_error.is_some() { "window_list_unavailable" } else { "window_not_attributed" },
            "error": match &ab_error {
                Some(e) => format!("hwnd {hwnd} could not be checked: ab would not list the desktop's windows ({e})"),
                None => format!("hwnd {hwnd} cannot be proved to belong to SOLIDWORKS"),
            },
            "abError": ab_error,
            "window": attributed,
            "mainHwnd": main,
            "_hint": "This verb closes SOLIDWORKS' own windows, and nothing else. ab's window \
                      list carries no owner pid, so a bare Win32 dialog class is not proof of \
                      ownership — closing it could hit another application's dialog. \
                      swx_window_info lists what IS attributable (`ownerConfirmed:true`), and \
                      swx_screenshot on `mainHwnd` captures SOLIDWORKS plus every popup it truly \
                      owns. For anything else, ab's own desktop verbs are the right tool and the \
                      user's approval prompt is the right gate.",
            "_reportIssues": crate::verbs::report_issues(),
        });
    }
    // A dialog that asks about the USER'S OWN WORK is never closed by this
    // bridge — "Save changes?", Document Recovery, licence. The sibling verb
    // (swx_dismiss_blocking_dialogs) has always refused these; this one had
    // neither that refusal nor the opt-in, and `swx_window_info` was
    // advertising the dialog as `closable:true`.
    let kind_of = attributed
        .as_ref()
        .and_then(|w| w.get("title").and_then(|t| t.as_str()))
        .map(crate::abwin::classify);
    if let Some((kind, _, never_auto)) = kind_of {
        if never_auto {
            return json!({
                "success": false,
                "errorCode": "dialog_belongs_to_the_user",
                "error": format!("hwnd {hwnd} is a '{kind}' dialog, which asks about the user's own work"),
                "window": attributed,
                "_hint": "This bridge does not answer questions about the user's unsaved work, \
                          their recovered documents, or their licence — closing such a dialog \
                          answers it, and by this bridge's own live evidence closing a modal \
                          SOLIDWORKS is blocked on can take the whole application down with \
                          every unsaved document in it. Ask the user to answer it.",
                "_reportIssues": crate::verbs::report_issues(),
            });
        }
    }
    // ...and closing ANY dialog needs the process-level opt-in, for the same
    // reason swx_dismiss_blocking_dialogs does. `confirm` and `reason` are both
    // things a caller writes for itself, which is not a sufficient basis for an
    // act that can end the user's session.
    if let Err(e) = crate::writes::authorize_in(
        ctx,
        args,
        "swx_close_window",
        crate::writes::Kind::EndSession,
        None,
        None,
    ) {
        return e;
    }
    let reason = arg_str(args, "reason").unwrap_or("").to_string();
    let r = crate::abwin::close_window(hwnd, &reason, AB_TIMEOUT);
    crate::ledger::record("window_closed", json!({"hwnd": hwnd, "reason": reason}));
    let ok_flag = r.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
    json!({
        "success": ok_flag,
        "hwnd": hwnd,
        "ab": r,
        "windowEtiquette": "background",
        "_hint": "Delegates to ab's desktop_ui_window (UIA WindowPattern.close) — background, \
                  no focus steal. Closing a modal that SOLIDWORKS is BLOCKED on has been \
                  observed to end the application; check swx_status afterwards.",
        "_reportIssues": crate::verbs::report_issues(),
    })
}

/// `swx_set_driving_badge` — the "Adom is driving SOLIDWORKS" taskbar overlay.
pub fn set_driving_badge(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let stored = crate::scratch::setting_bool("drivingBadge").unwrap_or(true);
    let Some(enabled) = args.get("enabled").and_then(|v| v.as_bool()) else {
        return ok(json!({
            "showDrivingBadge": stored,
            "_hint": "Read-only with no `enabled`. Pass enabled:true/false to change it; the \
                      setting is sticky and lives with the bridge, not with this conversation.",
        }));
    };
    crate::scratch::set_setting_bool("drivingBadge", enabled);
    let hwnd = main_hwnd(ctx);
    let ab = match hwnd {
        Some(h) => crate::abwin::set_badge(h, enabled, "Adom is driving SOLIDWORKS", AB_TIMEOUT),
        None => json!({"success": false, "error": "SOLIDWORKS window handle unavailable (is it running?)"}),
    };
    ok(json!({
        "showDrivingBadge": enabled,
        "applied": ab.get("success").and_then(|v| v.as_bool()).unwrap_or(false),
        "mainHwnd": hwnd,
        "ab": ab,
        "_hint": "A taskbar overlay so the user can see, at a glance, that an AI is driving \
                  their CAD session. Sticky. It is applied to SOLIDWORKS' own window via ab's \
                  desktop_taskbar; with SOLIDWORKS closed the preference is stored and applied \
                  the next time a verb finds a window.",
    }))
}

/// `swx_check_recovery` — is SOLIDWORKS showing the Document Recovery pane?
pub fn check_recovery(ctx: &Ctx, _args: &Value) -> Value {
    gate!(ctx);
    let com = ctx.call("check_recovery", json!({}), 30).unwrap_or_else(|e| json!({"error": e.message}));
    let (hwnd, pid) = window_identity(ctx);
    // "ab could not be asked" is NOT "there is no recovery pane". This verb
    // answers a question about a session that has just come back from a crash;
    // a fabricated `recoveryPending:false` is the worst available answer.
    let census = crate::abwin::list_windows(AB_TIMEOUT)
        .map(|all| crate::abwin::dialog_census(&all, hwnd, pid));
    let recovery: Option<Vec<Value>> = census.as_ref().ok().map(|dialogs| {
        dialogs
            .iter()
            .filter(|d| d.get("kind").and_then(|k| k.as_str()) == Some("document_recovery"))
            .cloned()
            .collect()
    });
    ok(json!({
        "recoveryDialogs": recovery.clone().map(Value::from).unwrap_or(Value::Null),
        "recoveryPending": recovery.as_ref().map(|r| !r.is_empty()).map(Value::from).unwrap_or(Value::Null),
        "recoveryKnown": recovery.is_some(),
        "dialogs": census.as_ref().ok().cloned().map(Value::from).unwrap_or(Value::Null),
        "abError": census.as_ref().err().cloned(),
        "com": com,
        "_hint": "SOLIDWORKS has no COM member that answers 'is the recovery pane up', so this \
                  is a WINDOW-level answer: a Document Recovery dialog on screen means the last \
                  session ended badly and the user has a decision to make. This bridge will not \
                  answer it for them.",
    }))
}

/// `swx_action_ledger` — what the bridge did in the background.
pub fn action_ledger(_ctx: &Ctx, args: &Value) -> Value {
    let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50).clamp(1, 500) as usize;
    let actions = crate::ledger::read(limit);
    ok(json!({
        "actions": actions.clone(),
        "count": actions.len(),
        "limit": limit,
        "_hint": "The append-only record of everything this bridge did on its own initiative: \
                  documents it created and closed, dialogs it closed, SOLIDWORKS kills. \
                  Chronological, oldest first. Works with SOLIDWORKS closed — it is a file in \
                  the bridge's own scratch dir, and swx_uninstall removes it.",
    }))
}

/// `swx_verb_times` — measured timings on THIS machine.
pub fn verb_times(_ctx: &Ctx, _args: &Value) -> Value {
    let stages = crate::ledger::stages();
    let count = stages.as_object().map(|m| m.len()).unwrap_or(0);
    ok(json!({
        "stages": stages,
        "verbCount": count,
        "_hint": "count/p50/p90/min/max seconds per verb, measured HERE — the honest input for \
                  an ETA or a progress bar, instead of a constant compiled into the bridge. \
                  Percentiles are nearest-rank, so every number shown is a run that really \
                  happened. Empty until slow verbs have run at least once.",
    }))
}

// ── documents: create, import, save-as, show ────────────────────────────────

/// `swx_new_document` — the verb Drew asked for. LIVE-PROVEN.
/// GIVE A NEWLY-CREATED DOCUMENT AN IDENTITY OF ITS OWN, immediately.
///
/// LIVE, 2026-09-11: `drain-gate.py` imported one STEP fixture twice and BOTH
/// documents came back with `path:""`. A fresh `LoadFile4` import has no path at
/// all — the `…\IC~~\…` conversion paths on the 1342 leaked temps came from a
/// different, older route — and SOLIDWORKS titles both imports from the same file
/// stem. The result was three open documents with one title and no paths, and
/// therefore NO WAY TO NAME ANY ONE OF THEM: `swx_save_as`, `swx_reset_workspace`
/// and `swx_close_doc` all refused with `ambiguous_target` (correctly — a title
/// is not an identity), the drain matched none of them, and the session could
/// only be cleared by hand.
///
/// A title is not an identity, so this bridge stops creating documents that have
/// nothing else. Immediately after a document is created, it is saved into the
/// bridge's own scratch directory under a unique name, which makes it:
/// addressable by path, ownable by path, drainable, closable, and impossible to
/// confuse with a second document of the same title.
///
/// THE TRADE-OFF, stated plainly: a file now exists on disk that did not before.
/// It is inside the bridge's own cache (`exports/scratch/`), it is removed by
/// `swx_uninstall`, and `keepUnsaved:true` opts out — at the price of a document
/// that may be unaddressable, which the response then says out loud.
/// IS THIS TITLE A NAME THAT RESOLVES TO EXACTLY ONE OPEN DOCUMENT?
///
/// Asked the way `CloseDoc` will answer it: SOLIDWORKS resolves a name
/// case-INSENSITIVELY on Windows, so `Part1.SLDPRT` and `part1.SLDPRT` are one
/// name to the act even though they are two strings to a comparison. A guard that
/// asks a different question from the act is not a guard.
///
/// This exists as one function because there are TWO places that close a
/// pathless document by title — the registry drain and the workspace sweep — and
/// a rule enforced in one of them is a rule with a hole in it.
fn title_resolves_uniquely(open_docs: &[Value], title: &str) -> bool {
    open_docs.iter().filter(|o| crate::verbs::title_stem(&doc_title(o)).eq_ignore_ascii_case(crate::verbs::title_stem(title))).count() == 1
}

fn give_identity(ctx: &Ctx, title: &str, reported_path: Option<&str>, kind: &str) -> Value {
    // Already has one: an `open`ed file, or a backend that pathed it for us.
    if reported_path.map(|p| !p.trim().is_empty()).unwrap_or(false) {
        return json!({"identity": "path", "path": reported_path, "title": title});
    }
    let stem = title.rsplit_once('.').map(|(a, _)| a).unwrap_or(title);
    let ext = match kind {
        "assembly" => "SLDASM",
        "drawing" => "SLDDRW",
        _ => "SLDPRT",
    };
    let dir = crate::scratch::scratch_docs_dir();
    // UNIQUE BY CONSTRUCTION: the session, the title stem and a counter. Two
    // imports of one fixture in one session must not collide, and a document
    // from a PREVIOUS session must not be overwritten by this one.
    let n = crate::scratch::next_scratch_seq();
    let file = format!("{}-{n}.{ext}", sanitise_stem(stem));
    let out = dir.join(&file);
    let out_s = out.to_string_lossy().to_string();
    // The ACTIVE document, deliberately unnamed in the call: SOLIDWORKS has just
    // activated what it created, and naming it by title is the very ambiguity
    // this exists to prevent. copy:false, because a COPY would write the file and
    // leave the open document as pathless as it was.
    match ctx.call("save_as", json!({"out": out_s, "copy": false}), 120) {
        Ok(r) => {
            let saved = r
                .get("savedAs")
                .and_then(|v| v.as_str())
                .map(String::from)
                .unwrap_or_else(|| out_s.clone());
            // THE TITLE CHANGES WITH THE FILE. A real Save As renames the open
            // document to the file it now is, so the caller — and the ownership
            // registry — must be told the name it will actually answer to.
            // READ BACK, never synthesized from the file name (0.10.10): with
            // Windows hiding known extensions (HideFileExt=1, the default)
            // SOLIDWORKS titles it `Part14-17`, otherwise `Part14-17.SLDPRT`.
            let new_title = r
                .pointer("/doc/title")
                .and_then(|t| t.as_str())
                .filter(|t| !t.trim().is_empty())
                .map(String::from)
                .unwrap_or_else(|| crate::comhost::mock::base_name(&saved).to_string());
            json!({
                "identity": "path",
                "path": saved,
                "title": new_title,
                "scratchFile": true,
            })
        }
        Err(e) => json!({
            "identity": "titleOnly",
            "path": Value::Null,
            "scratchFile": false,
            "warning": format!(
                "this document could not be given a file of its own ({}), so its TITLE is the only \
                 name it has — and a title is not an identity. If a second document of the same \
                 title is opened, neither can be addressed by any verb (`ambiguous_target`), \
                 including to close it. Save it somewhere with swx_save_as, or close it.",
                e.message
            ),
        }),
    }
}

/// A file name a filesystem will accept, from a document title.
fn sanitise_stem(stem: &str) -> String {
    let cleaned: String = stem
        .chars()
        .map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' })
        .collect();
    let trimmed = cleaned.trim_matches('_').to_string();
    if trimmed.is_empty() { "doc".into() } else { trimmed }
}

/// AFTER A HIDDEN CREATION: ISldWorks::DocumentVisible is a SESSION setting.
/// The host puts it back itself (three tries); if it reports it could not, the
/// server asks once more, and either way the outcome is ledgered — left false,
/// every document of that type the USER opens next would be invisible.
pub(crate) fn settle_document_visible(ctx: &Ctx, kind: &str, visibility: &Value) -> Value {
    let mut v = visibility.clone();
    if visibility.get("requested").and_then(|r| r.as_bool()) != Some(false) {
        return v;
    }
    let restored = visibility.get("documentVisibleRestored").and_then(|r| r.as_bool()) == Some(true);
    if restored {
        crate::ledger::record("document_visible_toggled", json!({"type": kind, "restored": true}));
        return v;
    }
    let retry = ctx.call("restore_document_visible", json!({"type": kind}), 30);
    let ok = retry.is_ok();
    v["serverRetry"] = json!({"restored": ok, "error": retry.err().map(|e| e.message)});
    v["documentVisibleRestored"] = json!(ok);
    crate::ledger::record(
        if ok { "document_visible_restored_on_retry" } else { "document_visible_restore_failed" },
        json!({"type": kind, "why": "ISldWorks::DocumentVisible is a session setting; left false, the user's next documents of this type open invisible"}),
    );
    if !ok {
        v["warning"] = json!(format!(
            "SOLIDWORKS's DocumentVisible setting for {kind} documents could NOT be put back: documents of that \
             type may open INVISIBLE in this session until SOLIDWORKS restarts. Tell the user."
        ));
    }
    v
}

// ── VERIFY BY EFFECT (0.11.3) ───────────────────────────────────────────────
// MEASURED 2026-10-15 22:40:50 (hosted 0.11.2): swx_new_document returned OK,
// with a title and a ledger `document_created` — and swx_docs one second later
// said []: SOLIDWORKS (pid 26316) had died right after the document command, 11 s
// after attach. A reply is not an effect. After a document-creating verb the
// bridge reads, from the LIVE session: the process is still the same one and
// still in the process census, the document count is not zero, and the document
// is in the list. Fresh sessions are read again after a short pause, because
// that death came a second after the reply.

/// The running session's (pid, creation time), from the host and the census.
pub(crate) fn session_identity(ctx: &Ctx) -> Option<(i64, Option<u64>)> {
    let s = ctx.call("startup_state", json!({}), 20).ok()?;
    if s.get("running").and_then(|v| v.as_bool()) != Some(true) {
        return None;
    }
    let pid = s.get("processId").and_then(|v| v.as_i64())?;
    let created = u32::try_from(pid)
        .ok()
        .and_then(|p| crate::launchos::os().processes().ok().and_then(|ps| ps.iter().find(|x| x.pid == p).and_then(|x| x.created_at)))
        .or_else(|| s.get("processCreatedAt").and_then(|v| v.as_u64()));
    Some((pid, created))
}

fn effect_recheck_ms() -> u64 {
    std::env::var("SWX_EFFECT_RECHECK_MS").ok().and_then(|v| v.trim().parse().ok()).unwrap_or(1500)
}

/// One reading of the effect: Ok(evidence), or Err((died, why)).
fn read_effect(ctx: &Ctx, before: (i64, Option<u64>), title: Option<&str>, path: Option<&str>) -> Result<Value, (bool, String)> {
    let (pid, created) = before;
    let census = crate::launchos::os().processes().ok();
    let in_census = census.as_ref().map(|ps| ps.iter().any(|p| p.pid as i64 == pid && (created.is_none() || p.created_at.is_none() || p.created_at == created)));
    // THE LIVE SESSION ANSWERS FIRST; the process census decides only when it
    // cannot (a dead SOLIDWORKS takes its COM server with it).
    let state = match ctx.call("startup_state", json!({}), 20) {
        Ok(s) => s,
        Err(e) => {
            if in_census == Some(false) {
                return Err((true, format!("the com host could not answer ({}) and SOLIDWORKS pid {pid} is gone from the process census", e.message)));
            }
            return Ok(json!({"verified": false, "processId": pid, "inProcessCensus": in_census,
                             "why": format!("the com host could not answer ({}); the process is {}", e.message,
                                            if in_census == Some(true) { "still running" } else { "of unknown state" })}));
        }
    };
    if state.get("running").and_then(|v| v.as_bool()) != Some(true) {
        return Err((true, format!("SOLIDWORKS (pid {pid}) is no longer running")));
    }
    let now_pid = state.get("processId").and_then(|v| v.as_i64());
    if now_pid != Some(pid) {
        return Err((true, format!("SOLIDWORKS pid {pid} is gone: the session now answering is pid {now_pid:?}")));
    }
    let count = state.get("documentCount").and_then(|v| v.as_u64());
    if count == Some(0) {
        return Err((false, "SOLIDWORKS reports ZERO open documents".into()));
    }
    let docs = ctx.call("docs", json!({}), 30).map_err(|e| (false, format!("the document list could not be read ({})", e.message)))?;
    // A list too long to enumerate (leaked import temps) cannot show the
    // document either way: the live count and the live process stand.
    if docs.get("truncated").and_then(|t| t.as_bool()) == Some(true) {
        return Ok(json!({"verified": true, "processId": pid, "processCreatedAt": created, "documentCount": count,
                         "inProcessCensus": in_census, "documentListTruncated": true}));
    }
    let list = docs.get("docs").and_then(|d| d.as_array()).cloned().unwrap_or_default();
    let found = list.iter().any(|d| {
        let dp = d.get("path").and_then(|p| p.as_str()).filter(|p| !p.is_empty());
        let dt = d.get("title").and_then(|t| t.as_str()).unwrap_or("");
        match (path, dp) {
            (Some(p), Some(q)) => crate::verbs::same_path(p, q),
            _ => title.map(|t| crate::verbs::same_title(t, dt)).unwrap_or(false),
        }
    });
    if !found && (title.is_some() || path.is_some()) {
        return Err((false, format!("the document ({}) is not in SOLIDWORKS' document list", path.or(title).unwrap_or("?"))));
    }
    Ok(json!({"verified": true, "processId": pid, "processCreatedAt": created, "documentCount": count, "inProcessCensus": in_census}))
}

/// THE EFFECT CHECK. `Ok(evidence)` to merge into the reply; `Err(reply)` —
/// `host_app_died` or `document_not_created` — never an ok.
pub(crate) fn verify_effect(ctx: &Ctx, verb: &str, before: Option<(i64, Option<u64>)>, title: Option<&str>, path: Option<&str>) -> Result<Value, Value> {
    let Some(before) = before else {
        return Ok(json!({"verified": false, "why": "the session's identity could not be read before the command"}));
    };
    let mut reads = 1;
    let mut r = read_effect(ctx, before, title, path);
    // A FRESH SESSION IS READ AGAIN: the 2026-10-15 death came a second after
    // the reply.
    let fresh = crate::readiness::attached_secs_ago(before.0, before.1).map(|s| s < 180).unwrap_or(false);
    if r.is_ok() && fresh && effect_recheck_ms() > 0 {
        std::thread::sleep(std::time::Duration::from_millis(effect_recheck_ms()));
        reads = 2;
        r = read_effect(ctx, before, title, path);
    }
    match r {
        Ok(mut v) => {
            v["reads"] = json!(reads);
            Ok(v)
        }
        Err((died, why)) => {
            let evidence = crate::perflog::crash_evidence();
            let code = if died { "host_app_died" } else { "document_not_created" };
            crate::ledger::record(
                code,
                json!({"verb": verb, "processId": before.0, "processCreatedAt": before.1, "documentReported": path.or(title),
                       "why": why, "crashEvidence": evidence}),
            );
            let mut out = err_code(
                code,
                if died {
                    format!("{verb} reported success, then SOLIDWORKS stopped: {why}. The document does not exist.")
                } else {
                    format!("{verb} reported success, but {why}: the document was NOT created")
                },
                if died {
                    "SOLIDWORKS died right after the command (measured 2026-10-15: 11 s after attach, a fresh start). \
                     Nothing was created. Quote `crashEvidence`; let the user restart it (swx_start), and wait for \
                     swx_status solidworks.ready before any document verb."
                } else {
                    "The command's reply is not the effect. swx_docs shows what is really open; retry once SOLIDWORKS \
                     is ready."
                },
            );
            if let Value::Object(m) = &mut out {
                m.insert("processId".into(), json!(before.0));
                m.insert("processCreatedAt".into(), json!(before.1));
                m.insert("documentReported".into(), json!(path.or(title)));
                m.insert("crashEvidence".into(), json!(evidence));
                m.insert("ledger".into(), json!(code));
            }
            Err(out)
        }
    }
}

pub fn new_document(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let kind = arg_str(args, "type").unwrap_or("part").to_string();
    // HIDDEN DOCUMENTS (0.10.6): `visible:false` creates it with no window
    // (ISldWorks::DocumentVisible around NewDocument). The default stays
    // visible — a user's document is one they want to see.
    let visible = match args.get("visible") {
        None | Some(Value::Null) => true,
        Some(Value::Bool(b)) => *b,
        Some(other) => {
            return err_code("bad_argument", format!("visible must be true or false, got {other}"), "Omit it for a visible document.")
        }
    };
    let job = ctx.jobs.start("new_document", format!("Creating a new {kind}"), 15);
    let mut call_args = json!({"type": kind, "template": args.get("template").cloned(), "visible": visible});
    // A HIDDEN document gets its scratch file from the host, through the very
    // document object it created: it is not necessarily the ACTIVE document, so
    // "save the active one" (give_identity) could name somebody else's.
    if !visible && !arg_bool(args, "keepUnsaved", false) {
        let ext = match kind.as_str() {
            "assembly" => "SLDASM",
            "drawing" => "SLDDRW",
            _ => "SLDPRT",
        };
        let n = crate::scratch::next_scratch_seq();
        let path = crate::scratch::scratch_docs_dir().join(format!("hidden-{}-{n}.{ext}", sanitise_stem(&kind)));
        call_args["scratchPath"] = json!(path.to_string_lossy());
    }
    let started = std::time::Instant::now();
    let session_before = session_identity(ctx);
    match ctx.call("new_document", call_args, 110) {
        // A FAILED new_document IS THE EXHAUSTION SIGNAL. On 2026-09-03 both the
        // part and the assembly template came back with no document at 17.4 GB
        // of private memory and 52 open documents, and the bridge reported it as
        // an ordinary template problem — which sent the caller looking for a
        // fault in a template that was fine. Look at what the machine is
        // actually holding before blaming the template.
        Err(e) => {
            // A HIDDEN CREATION THAT FAILED still flipped a session setting: it
            // must be back on before anything else happens.
            let settled = e.data.as_ref().and_then(|d| d.get("visibility")).map(|v| settle_document_visible(ctx, &kind, v));
            let mut out = match exhausted_error(ctx, &e) {
                Some(v) => v,
                None => sidecar_error(e),
            };
            if let (Some(v), Value::Object(m)) = (settled, &mut out) {
                m.insert("visibility".into(), v);
            }
            out
        }
        Ok(r) => {
            crate::ledger::record_time("swx_new_document", started.elapsed().as_secs_f64());
            let settled = r.get("visibility").map(|v| settle_document_visible(ctx, &kind, v));
            let mut identity = json!({"identity": "unknown"});
            if let Some(t) = r.get("created").and_then(|c| c.get("title")).and_then(|t| t.as_str()) {
                let t = t.to_string();
                let reported = r
                    .get("created")
                    .and_then(|c| c.get("path"))
                    .and_then(|p| p.as_str())
                    .filter(|p| !p.is_empty())
                    .map(String::from);
                // IDENTITY AT BIRTH, for the same reason as `import`: SOLIDWORKS
                // restarts its Part1/Part2 numbering every session, so a title is
                // not even unique across time, let alone across documents.
                identity = if arg_bool(args, "keepUnsaved", false) {
                    json!({
                        "identity": "titleOnly",
                        "path": reported,
                        "scratchFile": false,
                        "warning": "keepUnsaved:true — this document has only a TITLE. SOLIDWORKS \
                                    reuses Part1/Part2 every session, so a second document of that \
                                    title makes both unaddressable (ambiguous_target).",
                    })
                } else {
                    give_identity(ctx, &t, reported.as_deref(), &kind)
                };
                let session = ownership_context(ctx).0;
                let now_titled =
                    identity.get("title").and_then(|v| v.as_str()).unwrap_or(&t).to_string();
                match identity.get("path").and_then(|p| p.as_str()).filter(|p| !p.is_empty()) {
                    Some(p) => ctx.remember_scratch_at(&now_titled, session, p),
                    None => ctx.remember_scratch(&now_titled, session),
                }
            }
            // THE EFFECT, before anything is recorded as created (0.11.3).
            let made_title = identity.get("title").and_then(|v| v.as_str()).map(String::from)
                .or_else(|| r.get("created").and_then(|c| c.get("title")).and_then(|t| t.as_str()).map(String::from));
            let made_path = identity.get("path").and_then(|p| p.as_str()).filter(|p| !p.is_empty()).map(String::from);
            let effect = match verify_effect(ctx, "swx_new_document", session_before, made_title.as_deref(), made_path.as_deref()) {
                Ok(v) => v,
                Err(e) => {
                    if let Some(t) = &made_title {
                        ctx.forget_scratch_doc(t, made_path.as_deref());
                    }
                    return e;
                }
            };
            crate::ledger::record(
                "document_created",
                json!({
                    "title": r.get("created").and_then(|c| c.get("title")).cloned(),
                    "type": kind,
                    "template": r.get("template").cloned(),
                    "visible": visible,
                    "effect": effect,
                }),
            );
            let mut r = r;
            if let (Some(v), Some(o)) = (settled, r.as_object_mut()) {
                o.insert("visibility".into(), v);
            }
            if let Some(o) = r.as_object_mut() {
                if let Some(c) = o.get_mut("created").and_then(|v| v.as_object_mut()) {
                    if let Some(t) = identity.get("title").and_then(|v| v.as_str()) {
                        c.insert("title".into(), json!(t));
                    }
                    if let Some(p) =
                        identity.get("path").and_then(|p| p.as_str()).filter(|p| !p.is_empty())
                    {
                        c.insert("path".into(), json!(p));
                    }
                }
            }
            let titled_only = identity.get("identity").and_then(|v| v.as_str()) == Some("titleOnly");
            ok_merge(r, json!({
                "documentIdentity": identity,
                "effect": effect,
                "windowEtiquette": "background",
                "progress": job.finished(),
                "_hint": if !visible {
                    "A NEW HIDDEN document, with no window. It CANNOT BE MODELLED: SOLIDWORKS \
                     selects only in the active document and a document created invisible cannot \
                     be activated (measured 2026-10-14), so swx_run_script refuses modelling steps \
                     on it (document_hidden). Saving and exporting work. SOLIDWORKS documents that \
                     it cannot be shown later either; save it and reopen it to see it. Hiding is \
                     NOT a memory fix: to keep a battery flat, reuse one VISIBLE document \
                     (swx_run_script reuseDocument). The hidden-creation setting was put back \
                     (`visibility.documentVisibleRestored`)."
                } else if titled_only {
                    "A NEW document is open and active, with a TITLE and NO PATH — so it can only \
                     be named by that title, and SOLIDWORKS reuses Part1/Part2 every session. If a \
                     second document of this title appears, neither can be addressed by any verb, \
                     including to close it. swx_save_as gives it a name of its own."
                } else {
                    "A NEW document is open and active, and the bridge has already given it a file \
                     of its own in its scratch directory (`documentIdentity.path`) so it can be \
                     addressed, owned and closed by PATH rather than by a title two documents can \
                     share. Build geometry with swx_run_script, then swx_save_as to put it where \
                     the user wants it; swx_close_doc {save:false} throws it away, and \
                     swx_uninstall removes the scratch directory. Pass keepUnsaved:true to skip \
                     the scratch file and accept the ambiguity."
                },
            }))
        }
    }
}

/// `swx_import` — a neutral CAD file in as a new document (fusion_import_step).
/// A MUTATING CALL TIMED OUT. WHAT DID IT DO BEFORE IT STOPPED ANSWERING?
///
/// LIVE, 2026-09-11: an import whose com host timed out AFTER `LoadFile4` had
/// already succeeded left a document open that the bridge did not know about.
/// The verb reported failure — correctly, it could not confirm anything — and the
/// document was still there at the next baseline, owned by nobody, and (being
/// pathless and same-titled) unaddressable by any verb.
///
/// A timeout is "I do not know", and the answer to "I do not know" is to LOOK.
/// On the respawned host: list the documents, and adopt one that is unsaved, is
/// titled after the file we were importing, and was not open before the call. If
/// the list cannot be read, say so — never claim the session is clean.
///
/// Returns what to tell the caller: `None` when nothing was adopted and nothing
/// is in doubt.
fn reconcile_after_timeout(
    ctx: &Ctx,
    stem: &str,
    before_titles: &[String],
    before_count: Option<u64>,
) -> Option<Value> {
    let (session, open_docs, _active, truncated) = ownership_context(ctx);
    if truncated {
        return Some(json!({
            "reconciled": false,
            "adopted": Value::Null,
            "warning": "the call timed out AFTER it may have already opened the document, and the \
                        open-document list cannot be enumerated, so this bridge cannot tell whether \
                        a document was created. It is NOT claiming the session is unchanged. Run \
                        swx_docs (count and sample) and swx_close_import_temps.",
        }));
    }
    let stem_l = stem.to_ascii_lowercase();
    // EVERY ONE OF THESE, or nothing is adopted. Adoption makes this bridge
    // willing to CLOSE a document, so "probably ours" is not a standard it may
    // use — and the cheapest wrong answer here is claiming one of the user's.
    //
    //   * UNSAVED — an import that succeeded has not been saved by anyone;
    //   * the TITLE STEM matches the file we were importing;
    //   * it was NOT OPEN before our call (creation order, from the snapshot
    //     taken immediately before it);
    //   * and the open-document count really did rise by exactly one, so the
    //     session agrees that exactly one document appeared.
    let count_rose_by_one = match (before_count, open_docs.len() as u64) {
        (Some(b), now) => now == b + 1,
        // No count before the call: we cannot say the session gained exactly one.
        (None, _) => false,
    };
    let candidates: Vec<&Value> = open_docs
        .iter()
        .filter(|d| {
            let t = doc_title(d);
            let unsaved = !d.get("saved").and_then(|v| v.as_bool()).unwrap_or(false)
                && doc_path(d).filter(|p| !p.is_empty()).is_none();
            !before_titles.iter().any(|b| b.eq_ignore_ascii_case(&t))
                && t.to_ascii_lowercase().contains(&stem_l)
                && unsaved
        })
        .collect();
    if candidates.len() == 1 && !count_rose_by_one {
        return Some(json!({
            "reconciled": false,
            "adopted": Value::Null,
            "candidate": doc_title(candidates[0]),
            "warning": format!(
                "one document matching '{stem}' appeared after the timed-out call, but the \
                 open-document count did not rise by exactly one ({before_count:?} -> {}), so \
                 this bridge cannot say that document is the one its call created. It claims \
                 NOTHING — adoption is what lets it close a document, and it will not close one \
                 on a maybe. swx_docs lists what is open.",
                open_docs.len()
            ),
        }));
    }
    match candidates.len() {
        0 => None,
        1 => {
            let t = doc_title(candidates[0]);
            ctx.remember_scratch(&t, session);
            crate::ledger::record(
                "document_adopted_after_timeout",
                json!({"title": t, "stem": stem, "why": "a mutating call timed out after the                         document had already been created; this bridge now owns it and can close it"}),
            );
            Some(json!({
                "reconciled": true,
                "adopted": {"title": t},
                "evidence": {
                    "unsaved": true,
                    "titleStemMatches": stem,
                    "notOpenBeforeTheCall": true,
                    "openCountRoseByExactlyOne": true,
                },
                "warning": format!(
                    "the call timed out, but a document titled '{t}' appeared that was not open \
                     before it, is unsaved, matches the file being imported, and is the only \
                     document the session gained — the import had already succeeded when the com \
                     host stopped answering. This bridge has ADOPTED it, so swx_close_doc and \
                     swx_reset_workspace can now clean it up."
                ),
            }))
        }
        n => Some(json!({
            "reconciled": false,
            "adopted": Value::Null,
            "candidates": candidates.iter().map(|d| doc_title(d)).collect::<Vec<_>>(),
            "warning": format!(
                "the call timed out and {n} new unsaved documents matching '{stem}' are open — \
                 this bridge will NOT guess which (if any) is the one its call created, so it \
                 claims none of them. They may be unaddressable (same title, no path); ask the \
                 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."
            ),
        })),
    }
}

pub fn import(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let path = match arg_str(args, "filePath").or_else(|| arg_str(args, "path")) {
        Some(p) => p.to_string(),
        None => {
            return err(
                "filePath is required",
                "An absolute path on the DESKTOP filesystem to a STEP/IGES/Parasolid/STL file. \
                 From a container, push the bytes over with ab's write_file first. For a native \
                 .sldprt/.sldasm/.slddrw use swx_open instead — that is not an import.",
            )
        }
    };
    let job = ctx.jobs.start("import", format!("Importing {}", crate::comhost::mock::base_name(&path)), 60);
    let started = std::time::Instant::now();
    // WHAT WAS OPEN BEFORE, so a timeout can be reconciled against it.
    let before_docs = ownership_context(ctx).1;
    let before_titles: Vec<String> = before_docs.iter().map(doc_title).collect();
    let before_count = Some(before_docs.len() as u64);
    let session_before = session_identity(ctx);
    let mut host_args = json!({"filePath": path});
    if args.get("dissolve").and_then(|v| v.as_bool()).unwrap_or(false) {
        host_args["dissolve"] = json!(true);
    }
    // THE MEASUREMENT SURFACE, forwarded verbatim. These exist so the live
    // application can be asked which invocation actually imports a STEP on this
    // edition — nothing here interprets them, and `auto` is the default.
    for k in ["route", "openOptions", "docType", "withImportData", "argString"] {
        if let Some(v) = args.get(k) {
            host_args[k] = v.clone();
        }
    }
    match ctx.call("import", host_args, 280) {
        Err(e) if e.timed_out => {
            let stem = crate::comhost::mock::base_name(&path);
            let stem = stem.rsplit_once('.').map(|(a, _)| a).unwrap_or(&stem).to_string();
            let mut out = sidecar_error(e);
            // WHY DID IT STOP ANSWERING? On this install `swImportAutoRunImport-
            // Diagnostics` is ON (measured 2026-09-17), so a STEP with faulty
            // faces raises a MODAL Import Diagnostics dialog mid-import and the
            // COM call never returns. That is a dialog, not a hang, and the two
            // call for opposite responses from a caller — so it is NAMED. It is
            // never answered: the choice it offers changes the geometry the user
            // gets.
            if let Some(Some(d)) = blocking_dialog_now(ctx) {
                if let Value::Object(m) = &mut out {
                    m.insert("stoppedByDialog".into(), d.clone());
                    m.insert("stoppedBy".into(), json!("dialog"));
                    m.insert(
                        "_hint".into(),
                        json!(format!(
                            "SOLIDWORKS is showing a dialog and the import is waiting behind it:                              {d}. This bridge NEVER answers one — Import Diagnostics in particular                              offers to heal faulty faces, which changes the geometry you get, and                              that is the user's decision. swx_state takes a background screenshot;                              show the user and let them answer, then run swx_docs to see what                              ended up open."
                        )),
                    );
                }
            }
            if let Some(rec) = reconcile_after_timeout(ctx, &stem, &before_titles, before_count) {
                if let Value::Object(m) = &mut out {
                    m.insert("reconciliation".into(), rec);
                }
            }
            out
        }
        Err(e) => sidecar_error(e),
        Ok(r) => {
            crate::ledger::record_time("swx_import", started.elapsed().as_secs_f64());
            let mut identity = json!({"identity": "unknown"});
            if let Some(t) = r.get("imported").and_then(|c| c.get("title")).and_then(|t| t.as_str()) {
                // ONE ROW, NOT TWO. Recording the title AND then the path added a
                // second, pathless row for the same document — five imports made
                // ten rows, and on an unlistable session (where the prune is
                // correctly skipped) the phantoms accumulate: 1342 leaked temps
                // would have written 2684 rows on every registry save.
                // RECORD THE PATH WHEN THERE IS ONE. An imported document is unsaved but it
                // still HAS a path — SOLIDWORKS' `GetPathName` returns its
                // `…\Temp\swx<pid>\IC~~\…` conversion temp — and ownership is
                // decided BY PATH whenever a document has one (`doc_matches`).
                // Remembering only the title meant the registry held `path:None`
                // while the open document reported a path, the two never matched,
                // and the bridge refused to close the very document it had just
                // imported: "which this bridge did not create". The mock hid this
                // for a release by reporting an empty path for temps.
                // An imported document is unsaved but it still HAS a path —
                // `GetPathName` returns its `…\Temp\swx<pid>\IC~~\…` conversion
                // temp — and ownership is decided BY PATH whenever a document has
                // one (`doc_matches`). A title-only row would never match it.
                let session = ownership_context(ctx).0;
                let reported = r
                    .get("imported")
                    .and_then(|c| c.get("path"))
                    .and_then(|p| p.as_str())
                    .filter(|p| !p.is_empty())
                    .map(String::from);
                // IDENTITY AT BIRTH. A fresh import is PATHLESS live, and two
                // imports of one file share a title — see `give_identity`.
                identity = if arg_bool(args, "keepUnsaved", false) {
                    json!({
                        "identity": "titleOnly",
                        "path": reported,
                        "scratchFile": false,
                        "warning": "keepUnsaved:true — this document has only a TITLE, and a fresh \
                                    import has no path of its own. If another document of the same \
                                    title is opened, neither can be addressed by any verb \
                                    (ambiguous_target), including to close it.",
                    })
                } else {
                    give_identity(ctx, t, reported.as_deref(), "part")
                };
                let now_titled =
                    identity.get("title").and_then(|v| v.as_str()).unwrap_or(t).to_string();
                match identity.get("path").and_then(|p| p.as_str()).filter(|p| !p.is_empty()) {
                    Some(p) => ctx.remember_scratch_at(&now_titled, session, p),
                    None => ctx.remember_scratch(&now_titled, session),
                }
            }
            let made_title = identity.get("title").and_then(|v| v.as_str()).map(String::from)
                .or_else(|| r.get("imported").and_then(|c| c.get("title")).and_then(|t| t.as_str()).map(String::from));
            let made_path = identity.get("path").and_then(|p| p.as_str()).filter(|p| !p.is_empty()).map(String::from);
            let effect = match verify_effect(ctx, "swx_import", session_before, made_title.as_deref(), made_path.as_deref()) {
                Ok(v) => v,
                Err(e) => return e,
            };
            crate::ledger::record(
                "document_created",
                json!({"title": r.get("imported").and_then(|c| c.get("title")).cloned(), "via": "import", "effect": effect}),
            );
            let mut r = r;
            if let Some(o) = r.as_object_mut() {
                if let Some(imp) = o.get_mut("imported").and_then(|v| v.as_object_mut()) {
                    if let Some(t) = identity.get("title").and_then(|v| v.as_str()) {
                        imp.insert("title".into(), json!(t));
                    }
                    if let Some(p) =
                        identity.get("path").and_then(|p| p.as_str()).filter(|p| !p.is_empty())
                    {
                        // The path the document ACTUALLY has now, so a caller who
                        // reads `imported.path` can address it.
                        imp.insert("path".into(), json!(p));
                    }
                }
            }
            // A LINKED IMPORT IS A DIFFERENT THING AND MUST SAY SO FIRST. With
            // 3D Interconnect on — the user's setting, which this bridge reads
            // and never changes — the file was OPENED as a part holding a link
            // back to it, not translated into one. A caller told only "imported"
            // would be wrong about what moving or deleting the source file does.
            let linked = r.get("linked").and_then(|v| v.as_bool()).unwrap_or(false);
            let source = r.get("sourcePath").and_then(|v| v.as_str()).unwrap_or("the source file");
            let base = "It is a NEW open document, exactly like swx_new_document — it stays open \
                        and holds memory until you save it (swx_save_as) or close it \
                        (swx_close_doc). Importing repeatedly without closing is what leaks \
                        documents; swx_close_import_temps drains any left behind. \
                        `documentsOpened`/`netCountOk` report that exactly one document was \
                        added. swx_inspect_bodies tells you what actually arrived.";
            let hint = if linked {
                format!(
                    "OPENED AS A LINKED PART. This user's SOLIDWORKS has 3D Interconnect ON, so a \
                     neutral file is not translated — it is opened as a part holding a linked \
                     feature that points at {source}. The part is NOT self-contained: moving, \
                     editing or deleting the source file changes it. This bridge reads that \
                     preference and never changes it; `dissolve:true` is refused until the \
                     link-breaking API is measured on this install. {base}"
                )
            } else {
                format!(
                    "Imported by the translator (3D Interconnect is off), so the result is \
                     self-contained. {base} Which formats work depends on the install's \
                     translators — an unsupported one is a clear refusal, not a dialog."
                )
            };
            ok_merge(r, json!({
                "documentIdentity": identity,
                "windowEtiquette": "background",
                "progress": job.finished(),
                "_hint": hint,
            }))
        }
    }
}

/// `swx_save_as` — write the active document somewhere new.
pub fn save_as(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let out = match arg_str(args, "outputPath").or_else(|| arg_str(args, "out")) {
        Some(o) => match safe_out(o) {
            Ok(p) => p,
            Err(e) => return e,
        },
        None => {
            return err(
                "outputPath is required",
                "An absolute destination path, e.g. C:\\\\parts\\\\bracket.sldprt. This is how a \
                 document made by swx_new_document gets onto disk.",
            )
        }
    };
    if let Some(ext) = blocked_by_extension(&out) {
        return blocked_path_error(&out, ext);
    }
    let copy = arg_bool(args, "copy", false);
    // A plain (non-copy) save-as RETITLES the open document — that is the point
    // of it, and exactly how v0.2 silently renamed people's parts. Resolve the
    // target ONCE and send that identity down, so the document the gate
    // approved is the document the com host acts on.
    // Ownership and overwrite are BOTH decided by the choke point below; here
    // we only need the document's identity, resolved unambiguously.
    let resolved = resolve_document(ctx, args, "swx_save_as");
    let target = match resolved {
        Ok(t) => t,
        Err(e) => return e,
    };
    let grant = match crate::writes::authorize_in(
        ctx,
        args,
        "swx_save_as",
        if copy { crate::writes::Kind::ExportCopy } else { crate::writes::Kind::Rename },
        Some(&target),
        Some(&out),
    ) {
        Ok(g) => g,
        Err(e) => return e,
    };
    let mut call_args = crate::writes::with_grant(target.args(), &grant);
    if let Value::Object(m) = &mut call_args {
        m.insert("outputPath".into(), Value::from(out.clone()));
        m.insert("copy".into(), Value::from(copy));
    }
    let started = std::time::Instant::now();
    let was = target.title.clone();
    match ctx.call("save_as", call_args, 280) {
        Err(e) => sidecar_error(e),
        Ok(r) => {
            crate::ledger::record_time("swx_save_as", started.elapsed().as_secs_f64());
            crate::ledger::record("document_saved_as", json!({"document": was, "outputPath": r.get("savedAs"), "copy": copy}));
            // A rename keeps our claim on the document — under its new title AND
            // its new path. Record the path SOLIDWORKS REPORTS, not the one we
            // asked for: it normalises the case (`smoke.sldprt` comes back as
            // `smoke.SLDPRT`), and keeping the requested string is what made the
            // bridge disown its own scratch document the moment it saved it.
            if !copy && target.owned {
                if let Some(now) = r.get("doc").and_then(|d| d.get("title")).and_then(|t| t.as_str()) {
                    let reported = r
                        .get("doc")
                        .and_then(|d| d.get("path"))
                        .and_then(|p| p.as_str())
                        .filter(|s| !s.is_empty())
                        .map(String::from)
                        .unwrap_or_else(|| out.clone());
                    ctx.rename_scratch_doc(&was, target.path.as_deref(), now, Some(reported), true);
                }
            }
            ok_merge(r, json!({
                "_hint": "Default (copy:false) is a real Save As: the OPEN document is now this \
                          file and carries this name. copy:true writes the file and leaves the \
                          open document exactly as it was — that is what swx_export does for \
                          every neutral format.",
            }))
        }
    }
}

/// The `swx_show_*` family — bring a document to the front of SOLIDWORKS' own
/// window and fit it in view, without touching the user's desktop focus.
pub fn show(ctx: &Ctx, args: &Value, want: &'static str) -> Value {
    gate!(ctx);
    // Opening first is what makes `swx_show_part {path}` a one-call verb.
    if let Some(path) = arg_str(args, "path") {
        if let Some(t) = crate::comhost::mock::doc_type_of(path) {
            if t != want {
                return err_code(
                    "wrong_document_type",
                    format!("swx_show_{want} was given a {t}: {path}"),
                    format!("Use swx_show_{t} for that file, or swx_open plus swx_state."),
                );
            }
        }
        let already = ctx
            .call("docs", json!({}), 30)
            .ok()
            .and_then(|r| r.get("docs").and_then(|d| d.as_array()).cloned())
            .map(|docs| {
                docs.iter().any(|d| d.get("path").and_then(|p| p.as_str()) == Some(path))
            })
            .unwrap_or(false);
        if !already {
            if let Err(e) = ctx.call("open", json!({"path": path}), 280) {
                return sidecar_error(e);
            }
        }
    }
    let call_args = json!({"path": args.get("path").cloned(), "activate": arg_bool(args, "activate", true)});
    match ctx.call("show", call_args, 110) {
        Err(e) => sidecar_error(e),
        Ok(r) => {
            let doc_type = r.get("doc").and_then(|d| d.get("type")).and_then(|t| t.as_str()).unwrap_or("");
            if !doc_type.is_empty() && doc_type != want {
                return err_code(
                    "wrong_document_type",
                    format!("the active document is a {doc_type}, not a {want}"),
                    format!("swx_show_{doc_type} is the verb for it, or pass `path`."),
                );
            }
            let shot = if arg_bool(args, "capture", false) {
                main_hwnd(ctx).map(|h| crate::abwin::screenshot(h, Duration::from_secs(45)))
            } else {
                None
            };
            ok_merge(r, json!({
                "view": want,
                "screenshot": shot,
                "windowEtiquette": "background",
                "_hint": "Activated inside SOLIDWORKS and zoomed to fit — SOLIDWORKS' own window \
                          is never raised over whatever the user is doing. capture:true adds a \
                          background PrintWindow screenshot (via ab), which works even when the \
                          window is behind everything else.",
            }))
        }
    }
}

// ── design intelligence ─────────────────────────────────────────────────────

pub fn design_info(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let target = match resolve_document(ctx, args, "swx_design_info") {
        Ok(t) => t,
        Err(e) => return e,
    };
    match ctx.call("design_info", target.args(), 110) {
        Err(e) => sidecar_error(e),
        Ok(r) => ok_merge(r, json!({
            "_hint": "The design-health first look: features/sketches/bodies/components, the \
                      material, and whether anything is broken. featureErrors > 0 means numbers \
                      from swx_mass_properties or swx_bom are suspect — run swx_rebuild_check \
                      for the names.",
        })),
    }
}

pub fn get_parameters(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let target = match resolve_document(ctx, args, "swx_get_parameters") {
        Ok(t) => t,
        Err(e) => return e,
    };
    match ctx.call("get_parameters", target.args(), 110) {
        Err(e) => sidecar_error(e),
        Ok(r) => ok_merge(r, json!({
            "_hint": "Two sources in one list. `source:\"equation\"` rows are global variables \
                      from the equation manager (name + expression). `source:\"dimension\"` rows \
                      are named model dimensions — those are what swx_set_parameter drives, and \
                      their names look like D1@Boss-Extrude1. `value` is SI metres; `valueMm` is \
                      the same number in the unit an engineer says out loud.",
        })),
    }
}

pub fn set_parameter(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    if arg_str(args, "name").is_none() {
        return err(
            "name is required",
            "A dimension name as swx_get_parameters reports it, e.g. \"D1@Boss-Extrude1\".",
        );
    }
    if args.get("value").is_none() && args.get("valueMm").is_none() {
        return err(
            "value or valueMm is required",
            "valueMm is millimetres (what you probably mean); value is SI metres, matching what \
             swx_get_parameters returns. Passing both uses valueMm.",
        );
    }
    // Driving a dimension REBUILDS the model. Same rule as swx_run_script: not
    // on a document the bridge did not create, unless the caller says so — and
    // the resolved identity travels with the request.
    let target = match resolve_document(ctx, args, "swx_set_parameter") {
        Ok(t) => t,
        Err(e) => return e,
    };
    let grant = match crate::writes::authorize_in(ctx, args, "swx_set_parameter", crate::writes::Kind::ModifyDocument, Some(&target), None) {
        Ok(g) => g,
        Err(e) => return e,
    };
    let mut call_args = crate::writes::with_grant(target.args(), &grant);
    if let Value::Object(m) = &mut call_args {
        m.insert("name".into(), args.get("name").cloned().unwrap_or(Value::Null));
        m.insert("value".into(), args.get("value").cloned().unwrap_or(Value::Null));
        m.insert("valueMm".into(), args.get("valueMm").cloned().unwrap_or(Value::Null));
    }
    match ctx.call("set_parameter", call_args, 280) {
        Err(e) => sidecar_error(e),
        Ok(r) => {
            crate::ledger::record("parameter_set", json!({"name": r.get("name"), "value": r.get("value")}));
            ok_merge(r, json!({
                "_hint": "Changed IN MEMORY and rebuilt. `before` and `value` let you verify the \
                          model actually moved; swx_save persists it, and without that the next \
                          close discards the change. A rebuild error afterwards shows up in \
                          swx_rebuild_check.",
            }))
        }
    }
}

/// `swx_add_component` — insert component instances into an assembly.
///
/// The verb the assembly surface was missing. Everything downstream of it —
/// `swx_bom`, `swx_interference`, per-component mass properties — needed an
/// assembly with components in it, and until this existed there was no way to
/// make one from a container.
///
/// Positions are MILLIMETRES, absolute in the assembly's coordinate system, and
/// the whole batch goes in ONE call: a crate of 150 fasteners is 150 COM
/// insertions but one round trip.
pub fn add_component(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    // Parse BEFORE resolving or authorizing: a malformed request must not open
    // with a COM round trip, and the caller gets the grammar back.
    let comps = match crate::script::parse_components(args) {
        Ok(c) => c,
        Err(e) => {
            return err(
                e,
                "swx_add_component {\"targetDocument\":\"Assem1\",\"components\":[{\"filePath\":\
                 \"C:\\\\parts\\\\rail.sldprt\",\"xMm\":0,\"yMm\":0,\"zMm\":0}]} — filePath is an \
                 absolute path on the DESKTOP filesystem, positions are millimetres in the \
                 assembly's coordinate system, and `configuration` picks a non-default \
                 configuration of the component.",
            )
        }
    };
    // Inserting a component MODIFIES the assembly, so it faces the same rule as
    // every other geometry change: not on a document the bridge did not create.
    // The component FILES are only read — the same reason an export from the
    // user's part is free — so they are not part of the gate.
    let target = match resolve_document(ctx, args, "swx_add_component") {
        Ok(t) => t,
        Err(e) => return e,
    };
    let grant = match crate::writes::authorize_in(
        ctx,
        args,
        "swx_add_component",
        crate::writes::Kind::ModifyDocument,
        Some(&target),
        None,
    ) {
        Ok(g) => g,
        Err(e) => return e,
    };
    let mut call_args = crate::writes::with_grant(target.args(), &grant);
    if let Value::Object(m) = &mut call_args {
        m.insert("components".into(), args.get("components").cloned().unwrap_or(Value::Null));
        // The single-component shorthand travels as the keys themselves.
        for k in [
            "filePath",
            "file",
            "xMm",
            "yMm",
            "zMm",
            "rotXDeg",
            "rotYDeg",
            "rotZDeg",
            "configuration",
            "fix",
        ] {
            if let Some(v) = args.get(k) {
                m.insert(k.into(), v.clone());
            }
        }
    }
    let job = ctx.jobs.start(
        "add_component",
        format!("Inserting {} component(s)", comps.len()),
        10 + comps.len() as u64,
    );
    let started = std::time::Instant::now();
    // 8s per component is generous for a warm install and survives a cold one
    // loading each part document for the first time.
    let timeout = (60 + 8 * comps.len() as u64).min(900);
    match ctx.call("add_component", call_args, timeout) {
        Err(e) => sidecar_error(e),
        Ok(r) => {
            crate::ledger::record_time("swx_add_component", started.elapsed().as_secs_f64());
            crate::ledger::record(
                "components_added",
                json!({
                    "assembly": target.title,
                    "insertedCount": r.get("insertedCount"),
                    "failedCount": r.get("failedCount"),
                    "method": r.get("method"),
                }),
            );
            let failed = r.get("failedCount").and_then(|v| v.as_i64()).unwrap_or(0);
            ok_merge(r, json!({
                "windowEtiquette": "background",
                "progress": job.finished(),
                "_hint": if failed > 0 {
                    "Some components did not go in — `failed[]` carries every insert call that \
                     was tried and what COM said. Success is measured by the assembly's \
                     component COUNT rising, not by the insert call's return value: \
                     AddComponent5 hands back Nothing on SOLIDWORKS Connected even when it \
                     worked, which is why this verb counts instead of trusting it. Changed IN \
                     MEMORY: swx_save persists the assembly."
                } else {
                    "Inserted, IN MEMORY — swx_save persists the assembly. `placedCount` is how \
                     many got the exact transform asked for: AddComponent5's X/Y/Z is NOT the \
                     component's origin, so the bridge writes each component's transform itself \
                     and `xMm/yMm/zMm` mean 'put the component's ORIGIN here'. Verify with \
                     swx_bom (what is in it, with quantities and custom-property columns) and \
                     swx_interference (count:0 is clean). `method` names the COM call that \
                     worked on this install; the bridge remembers it, so the next batch pays \
                     one call per component."
                },
            }))
        }
    }
}

pub fn inspect_bodies(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let target = match resolve_document(ctx, args, "swx_inspect_bodies") {
        Ok(t) => t,
        Err(e) => return e,
    };
    let mut call_args = target.args();
    if let Value::Object(m) = &mut call_args {
        m.insert("nameContains".into(), args.get("nameContains").cloned().unwrap_or(Value::Null));
    }
    match ctx.call("inspect_bodies", call_args, 130) {
        Err(e) => sidecar_error(e),
        Ok(r) => ok_merge(r, json!({
            "_hint": "GEOMETRY READ-BACK in MILLIMETRES — what actually got built, per solid \
                      body: bounding box, size, volume, area, mass, face count. This is the verb \
                      that tells you a modeling script did what you meant. Parts only; for an \
                      assembly use swx_bom (what is in it) or swx_mass_properties (what it \
                      weighs).",
        })),
    }
}

pub fn run_script(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let script = match args.get("script") {
        Some(s) if !s.is_null() => s.clone(),
        _ => return script_help("script is required"),
    };
    // Parse + validate HERE, before anything touches the live session: a script
    // that cannot work must never half-run on someone's CAD application.
    let mut ops = match crate::script::parse(&script) {
        Ok(o) => o,
        Err(e) => return script_help(e),
    };
    let mut script = script;
    // REUSE ONE DOCUMENT FOR A BATTERY (0.10.6). Measured 2026-10-14: every
    // created-and-closed document cost SOLIDWORKS ~83 MB and ~125 handles for
    // good. `reuseDocument` runs the script in a part the BRIDGE created, its
    // leading new_part becoming clear_part — N checks, ONE document.
    let reuse = arg_str(args, "reuseDocument").map(String::from);
    if let Some(r) = &reuse {
        if ops.iter().any(|o| matches!(o, crate::script::Op::NewAssembly { .. })) {
            return script_help("reuseDocument reuses a PART: a script that makes an assembly cannot run in it");
        }
        let news = ops.iter().filter(|o| matches!(o, crate::script::Op::NewPart { .. })).count();
        let leads = matches!(ops.first(), Some(crate::script::Op::NewPart { .. }));
        if news > 1 || (news == 1 && !leads) {
            return script_help("with reuseDocument, new_part may only be the FIRST step (it becomes clear_part)");
        }
        if leads {
            ops[0] = crate::script::Op::ClearPart;
            if let Some(first) = script.as_array_mut().and_then(|a| a.first_mut()) {
                *first = json!({"op": "clear_part"});
            }
        }
        let _ = r;
    }
    if let Some(m) = crate::script::hidden_conflict(&ops) {
        return err_code(
            "document_hidden",
            m,
            "Nothing was run. A hidden document (visible:false) can be created, saved and exported, but \
             not modelled. Scripts default to a visible part; for a battery that must not grow \
             SOLIDWORKS's memory, create ONE part and pass its path as reuseDocument.",
        );
    }
    let makes_its_own_doc =
        matches!(ops.first(), Some(crate::script::Op::NewPart { .. } | crate::script::Op::NewAssembly { .. }));
    let has_doc = ctx
        .call("docs", json!({}), 30)
        .ok()
        .and_then(|r| r.get("docs").and_then(|d| d.as_array()).map(|a| !a.is_empty()))
        .unwrap_or(false);
    if let Err(e) = crate::script::validate(&ops, has_doc) {
        return script_help(e);
    }
    // OWNERSHIP. A script that does not start by creating its own document
    // builds geometry into whatever is already open — which, unless the bridge
    // made it, is the user's model. Sketches and extrusions are not something
    // to add to somebody's part on the strength of an unqualified verb call,
    // and there is no undo: `swx_close_doc {save:false}` would throw away THEIR
    // document, not just our changes.
    let mut pinned: Option<String> = None;
    let mut grant = crate::writes::Grant { host_args: json!({}), user_write: false };
    // WHO MADE THIS DOCUMENT is decided by the SAME gate every other verb uses
    // (`resolve_document` → `owns_doc`, by PATH when it has one). Issue #2
    // (Michael, 2026-10-13): a part made by swx_new_document (scratchFile:true,
    // saved under exports\scratch) was reported bridgeCreatedDocument:false,
    // because this verb compared the reply's TITLE against the registry's
    // titles instead.
    let mut target_owned: Option<bool> = None;
    let mut pinned_path: Option<String> = None;
    if !makes_its_own_doc {
        let target_args = match &reuse {
            Some(r) if r.contains('\\') || r.contains('/') => json!({"path": r}),
            Some(r) => json!({"targetDocument": r}),
            None => args.clone(),
        };
        let target = match resolve_document(ctx, &target_args, "swx_run_script") {
            Ok(t) => t,
            Err(mut e) => {
                // SAY WHICH ARGUMENT NAMED IT (measured 2026-10-14: a reused part
                // that could not be found read "needs a document").
                if let (Some(r), Value::Object(m)) = (&reuse, &mut e) {
                    let inner = m.get("error").and_then(|v| v.as_str()).unwrap_or("").to_string();
                    m.insert(
                        "error".into(),
                        json!(format!("the document named by reuseDocument ('{r}') is not open, or not visible: {inner}")),
                    );
                }
                return e;
            }
        };
        if reuse.is_some() && !target.owned {
            return err_code(
                "reuse_not_owned",
                format!("reuseDocument names '{}', which this bridge did not create", target.title),
                "reuseDocument clears and rebuilds the part, so it only ever names a part the bridge made \
                 (swx_new_document {\"type\":\"part\",\"visible\":false} makes one). Nothing was changed.",
            );
        }
        // CLEAR_PART DELETES EVERY FEATURE: only in a part the bridge created,
        // whatever write grant the call carries (0.10.6).
        let clears_before_making = ops
            .iter()
            .position(|o| matches!(o, crate::script::Op::ClearPart))
            .map(|i| !ops[..i].iter().any(|o| matches!(o, crate::script::Op::NewPart { .. })))
            .unwrap_or(false);
        if clears_before_making && !target.owned {
            return err_code(
                "clear_part_not_owned",
                format!("clear_part deletes every feature, and '{}' is not a part this bridge created", target.title),
                "clear_part only ever runs in the bridge's own scratch parts. Nothing was changed.",
            );
        }
        target_owned = Some(target.owned);
        pinned_path = target.path.clone();
        grant = match crate::writes::authorize_in(ctx, args, "swx_run_script", crate::writes::Kind::ModifyDocument, Some(&target), None) {
            Ok(g) => g,
            Err(e) => return e,
        };
        pinned = Some(target.title);
    }
    // A `save_as` STEP writes a file; its destination faces the same rule.
    for op in &ops {
        if let crate::script::Op::SaveAs { path } = op {
            if let Err(e) = crate::writes::authorize_in(ctx, args, "swx_run_script (save_as step)", crate::writes::Kind::ExportCopy, None, Some(path)) {
                return e;
            }
        }
    }
    let job = ctx.jobs.start("run_script", format!("Running {} modeling step(s)", ops.len()), 30);
    let started = std::time::Instant::now();
    // The pinned title travels with the request so the com host targets the SAME
    // document for every step, instead of re-resolving "whatever is active now"
    // — a dialog, or the user clicking another window, must not be able to move
    // a running script onto a different model halfway through.
    let call_args = crate::writes::with_grant(json!({"script": script, "targetDocument": pinned, "targetPath": pinned_path}), &grant);
    match ctx.call("run_script", call_args, 280) {
        Err(e) => {
            // A HIDDEN new_part that failed still flipped a session setting.
            let settled = e.data.as_ref().and_then(|d| d.get("visibility")).map(|v| settle_document_visible(ctx, "part", v));
            // A SCRIPT THAT FAILED AFTER ITS new_part STILL MADE THAT PART: the
            // bridge owns it, so swx_close_doc / swx_reset_workspace can close it
            // (0.10.2 — a failed self-test check left one open, un-closable).
            if let Some(t) = e.created.as_ref().and_then(|c| c.get("title")).and_then(|t| t.as_str()) {
                ctx.remember_scratch(t, ownership_context(ctx).0);
                crate::ledger::record("document_created", json!({"title": t, "via": "run_script (before a failed step)"}));
            }
            let created = e.created.clone();
            let hidden_reuse = reuse.is_some() && e.code.as_deref() == Some("document_hidden");
            let mut out = sidecar_error(e);
            if let (true, Some(r), Value::Object(m)) = (hidden_reuse, &reuse, &mut out) {
                let inner = m.get("error").and_then(|v| v.as_str()).unwrap_or("").to_string();
                m.insert("error".into(), json!(format!("the document named by reuseDocument ('{r}') is hidden: {inner}")));
            }
            if let (Value::Object(m), Some(c)) = (&mut out, created) {
                m.insert("createdBeforeFailing".into(), c);
            }
            if let (Value::Object(m), Some(v)) = (&mut out, settled) {
                m.insert("visibility".into(), v);
            }
            let made = created_title_of(out.get("createdBeforeFailing"));
            note_left_open(&mut out, made);
            out
        }
        Ok(mut r) => {
            crate::ledger::record_time("swx_run_script", started.elapsed().as_secs_f64());
            // WHAT IS MEASURED (0.10.4): each step says whether the variant it ran
            // was measured live on THIS SOLIDWORKS release — one table,
            // compat::OP_FACTS (docs/LIVE-RESULTS.md).
            attach_proofs(&mut r, &ops);
            let made = created_title_of(r.get("created").filter(|c| !c.is_null()));
            note_left_open(&mut r, made);
            // HIDDEN new_part steps (0.10.6): the session setting, settled and
            // ledgered per step.
            if let Some(steps) = r.get_mut("steps").and_then(|s| s.as_array_mut()) {
                for st in steps.iter_mut() {
                    let kind = if st.get("op").and_then(|o| o.as_str()) == Some("new_assembly") { "assembly" } else { "part" };
                    if let Some(v) = st.pointer("/result/visibility").cloned() {
                        st["result"]["visibility"] = settle_document_visible(ctx, kind, &v);
                    }
                }
            }
            if let Some(t) = r
                .get("created")
                .and_then(|c| c.get("created").and_then(|x| x.get("title")).or_else(|| c.get("title")))
                .and_then(|t| t.as_str())
            {
                ctx.remember_scratch(t, ownership_context(ctx).0);
            }
            if let Some(created) = r.get("created").filter(|c| !c.is_null()) {
                crate::ledger::record(
                    "document_created",
                    json!({"title": created.get("created").and_then(|c| c.get("title")).or_else(|| created.get("title")), "via": "run_script"}),
                );
            }
            let ours = match target_owned {
                Some(owned) => owned,
                // The script made its own document: ours if the registry took it
                // (decided by owns_doc, with the path the reply reports).
                None => {
                    let (session, _, _, _) = ownership_context(ctx);
                    let title = r.get("documentName").and_then(|t| t.as_str()).unwrap_or("");
                    let path = r.get("doc").and_then(|d| d.get("path")).and_then(|p| p.as_str());
                    ctx.owns_doc(title, path, session)
                }
            };
            ok_merge(r, json!({
                "progress": job.finished(),
                "ops": crate::script::OPS,
                "bridgeCreatedDocument": ours,
                "_hint": if ours {
                    "Each step reports what it built. Verify with swx_inspect_bodies (the \
                     geometry) or swx_mass_properties (the mass) rather than trusting the step \
                     list. This document was created by the bridge, so swx_close_doc \
                     {save:false} discards the whole thing safely; swx_save_as puts it on disk. \
                     A step that fails stops the script and says which one, leaving what was \
                     already built in place."
                } else {
                    "Each step reports what it built. Verify with swx_inspect_bodies rather than \
                     trusting the step list. WARNING: this ran against a document the bridge did \
                     NOT create, so there is no safe undo — do NOT reach for swx_close_doc \
                     {save:false}, which would discard the user's whole document and not just \
                     these features. Undo belongs to the user, in SOLIDWORKS."
                },
            }))
        }
    }
}

fn created_title_of(created: Option<&Value>) -> Option<String> {
    let c = created?;
    c.get("created")
        .and_then(|x| x.get("title"))
        .or_else(|| c.get("title"))
        .and_then(|t| t.as_str())
        .map(String::from)
}

/// NEVER ACCUMULATE SILENTLY (0.10.9): a script that made a document while the
/// user is at the machine says, in its reply, that the document is still open
/// and why — so a battery that cannot close it stops instead of piling windows
/// up (measured 2026-10-14: 18 of them on Drew's screen).
fn note_left_open(out: &mut Value, title: Option<String>) {
    let Some(title) = title else { return };
    let a = crate::presence::activity();
    if !a.active {
        return;
    }
    if let Value::Object(m) = out {
        m.insert("documentsLeftOpen".into(), json!([title]));
        m.insert("why".into(), json!("user_present"));
        m.insert("userPresence".into(), a.to_json());
    }
}

/// `proven` on every step of a run_script reply, from compat::step_proof and
/// the release the host reports (`solidworksRevision`).
fn attach_proofs(r: &mut Value, ops: &[crate::script::Op]) {
    let major = r.get("solidworksRevision").and_then(|v| v.as_str()).and_then(crate::compat::major_of);
    if let Some(steps) = r.get_mut("steps").and_then(|s| s.as_array_mut()) {
        for st in steps.iter_mut() {
            let Some(op) = st.get("step").and_then(|n| n.as_u64()).and_then(|n| (n as usize).checked_sub(1)).and_then(|i| ops.get(i)) else {
                continue;
            };
            let proof = crate::compat::step_proof(op, st.get("result").unwrap_or(&Value::Null), major);
            match st.get_mut("result") {
                Some(Value::Object(m)) => {
                    m.insert("proven".into(), proof);
                }
                _ => st["result"] = json!({"proven": proof}),
            }
        }
    }
}

/// One place for the DSL's documentation, so a rejected script teaches the
/// caller the grammar instead of just saying no.
fn script_help(message: impl Into<String>) -> Value {
    let mut v = err_code(
        "bad_script",
        message,
        "swx_run_script takes `script`: a JSON ARRAY of steps, not a string of code. SOLIDWORKS \
         has no in-process scripting host the way Fusion has Python, so this bridge exposes a \
         small typed DSL over the modeling calls proven to work on a live install. Every \
         dimension is in MILLIMETRES.",
    );
    if let Value::Object(map) = &mut v {
        map.insert("ops".into(), json!(crate::script::OPS));
        map.insert(
            "verbArgs".into(),
            json!({
                "script": "the steps (required)",
                "targetDocument / path": "the document a script with no new_part runs in",
                "reuseDocument": "the title or path of a part the BRIDGE created: the script runs in it and its leading new_part becomes clear_part — a battery of N checks costs ONE document (each created-and-closed document cost SOLIDWORKS ~83 MB, measured 2026-10-14)",
            }),
        );
        map.insert(
            "example".into(),
            json!({
                "script": [
                    {"op": "new_part"},
                    {"op": "sketch_rect", "plane": "Front Plane", "widthMm": 50, "heightMm": 30},
                    {"op": "extrude", "depthMm": 20},
                    {"op": "set_material", "name": "6061 Alloy"},
                    {"op": "set_prop", "name": "PartNo", "value": "BRK-100"},
                    {"op": "save_as", "path": "C:\\parts\\bracket.sldprt"}
                ],
                "moreExamples": "examples/lego-2x4.json on the bridge page: a LEGO 2x4 brick in 4 features (offset-plane studs, a multi-profile sketch for the tubes)"
            }),
        );
        map.insert(
            "opArgs".into(),
            json!({
                "new_part": {"visible": "true (default) | false — created HIDDEN (ISldWorks::DocumentVisible). A hidden part CANNOT BE MODELLED (SOLIDWORKS selects only in the active document, and a hidden one cannot be activated — measured): a script that models it is refused document_hidden. Use reuseDocument, not hiding, to keep memory flat"},
                "new_assembly": {"visible": "as new_part"},
                "clear_part": {"(none)": "deletes EVERY feature after the origin of a part the BRIDGE created — never anyone else's; N checks then cost one document"},
                "sketch": {"plane": "Front Plane|Top Plane|Right Plane|<any plane name> | {offsetFrom, offsetMm} | {offsetFrom: {face: {atPointMm:[x,y,z]}}, offsetMm} (offsetMm along the face's OUTWARD normal; negative = into the part)", "face": "{atPointMm:[x,y,z]} instead of plane", "entities": "[{rect:{widthMm, heightMm, xMm, yMm, anchor:corner|center, centerMm:[x,y], cornerRadiusMm?}}, {circle:{radiusMm|diameterMm, xMm, yMm | centerMm}}, {line:{from:[x,y], to:[x,y]}}, {centerline:{from, to}}, {polyline:{points:[[x,y],...], close:bool}}, {arc:{center:[x,y], radiusMm, startDeg, endDeg, ccw:true}}, {arc3:{start, mid, end}}, {tangentArc:{to:[x,y], radiusMm?}} (continues the entity before it), {slot:{center, lengthMm (centre to centre), widthMm, angleDeg}}, {polygon:{center, sides, circumradiusMm | inscribedRadiusMm, angleDeg (first vertex)}}, {sketchFillet:{at:[x,y], radiusMm}} / {sketchChamfer:{at:[x,y], distanceMm}} (round or cut the corner where two LINES drawn before it meet), {offsetEntities:{fromFace:{atPointMm:[x,y,z]}, offsetMm (NEGATIVE = into the face), loops:outer|all, chain:true}} (0.11.4: a model face's outline offset into the sketch by SOLIDWORKS, the side checked; not yet run live)] — ALL in one sketch: concentric circles make a ring, a rect with circles inside makes a plate with holes. Lines and arcs must CLOSE (each end meets exactly one other within 1e-6 mm — an open contour is refused naming the gap); several closed contours are several regions. Any entity takes construction:true (never part of a profile). 0.11.0, not yet run live"},
                "sketch_rect": {"plane": "as sketch", "face": "as sketch", "widthMm": "required", "heightMm": "required", "xMm": 0, "yMm": 0, "anchor": "corner (default: xMm/yMm is the CORNER) | center", "centerMm": "[x, y] (implies center)"},
                "sketch_circle": {"plane": "as sketch", "face": "as sketch", "radiusMm": "required (or diameterMm)", "xMm": 0, "yMm": 0, "centerMm": "[x, y]"},
                "extrude": {"depthMm": "required unless throughAll", "reverse": false, "direction": "one|both|midPlane", "depth2Mm": "direction both: the other side (default = depthMm)", "midPlane": false, "throughAll": false},
                "cut": {"depthMm": "required unless throughAll", "throughAll": false, "reverse": false, "direction": "one|both|midPlane", "depth2Mm": "as extrude", "midPlane": false},
                "extrude / cut end conditions (0.11.0, not yet run live)": {"upToNext": "true — to the next face (swEndCondUpToNext 11)", "upToSurface": "{face:{atPointMm:[x,y,z]}} — to that face (swEndCondUpToSelection 10)", "offsetFromSurface": "{face:{atPointMm}, distanceMm, reverseOffset:false} — distanceMm short of that face (swEndCondOffsetFromSurface 5)", "draftDeg": "a taper, 0..90", "draftOutward": "false (default: the profile shrinks as it goes)"},
                "extrude / cut regions (0.11.4, not yet run live)": {"regionsAtMm": "[[u, v], ...] — only the sketch regions holding these points (the sketch's own coordinates): SOLIDWORKS' Selected Contours, for contours that CROSS (an offset outline and a circle). regionAtMm: [u, v] for one"},
                "revolve": {"axis": "\"x\" | \"y\" (the sketch's own axes) | {entity: i} (the sketch's i-th entity as written, 0-based: a line or centerline) | {line: [[x,y],[x,y]]} (added as a centerline)", "angleDeg": 360, "midPlane": false, "reverse": false, "_": "the profile must lie on ONE side of the axis; FeatureRevolve2. 0.11.0, not yet run live"},
                "revolve_cut": {"_": "as revolve, removing material (FeatureRevolve2 IsCut)"},
                "mirror": {"plane": "a plane name | {offsetFrom, offsetMm}", "face": "{atPointMm:[x,y,z]} — a planar face, instead of plane", "features": "\"last\" (default) | a name | [names] | \"all\" (= mirrorBody)", "mirrorBody": "true: every solid body", "_": "InsertMirrorFeature2 (features mark 1, plane mark 2, bodies mark 256). 0.11.0, not yet run live"},
                "hole": {"atMm": "[x,y,z] ON a planar face: the hole's centre", "diameterMm": "required", "depthMm": "required unless throughAll (flat bottom, measured from the face)", "throughAll": false, "counterbore": "{diameterMm, depthMm}", "countersink": "{diameterMm, angleDeg: 90 (included)}", "_": "built as cuts of circles on the face's plane (no Hole Wizard); one or the other of counterbore/countersink. 0.11.0, not yet run live"},
                "fillet": {"radiusMm": "required", "edges": "{atPointsMm:[[x,y,z],...]} | {allOfFeature: \"Boss-Extrude1\" | \"last\"}"},
                "chamfer": {"distanceMm": "required", "angleDeg": 45, "edges": "as fillet"},
                "shell": {"thicknessMm": "required", "removeFacesAtPointsMm": "[[x,y,z], ...] — a point ON each face to remove", "outward": false},
                "linear_pattern": {"feature": "\"last\" (default) | a feature name", "directions": "[{axis: x|y|z, count: n (incl. the original), spacingMm: s (negative = the other way)}] — one or two"},
                "circular_pattern": {"feature": "\"last\" | a feature name", "axis": "x|y|z", "count": "n (incl. the original)", "angleDeg": "360 (total, equally spaced)"},
                "set_appearance": {"rgb": "[r, g, b] 0..255", "target": "part (default) | {bodyName} | {faceAtPointMm:[x,y,z]}"},
                "set_material": {"name": "required unless densityKgM3", "database": "SOLIDWORKS Materials.sldmat", "densityKgM3": "with name: the material's density must be this (else material_density_mismatch); alone: the part's own density (no library material may be applied). The step reports densityKgM3 READ BACK"},
                "set_prop": {"name": "required", "value": "required"},
                "save_as": {"path": "required, absolute"},
                "rebuild": {},
                "zoom_fit": {},
            }),
        );
        map.insert(
            "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, 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, 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.",
                "Fillet/chamfer edges are de-duplicated before they are selected (an edge selected twice is deselected); points must lie ON one edge / one face (a corner or an edge between two faces is refused, never guessed).",
                "A sketch need not be the step right before its extrude/cut: the bridge re-selects it by name.",
                "\"last\" is the feature the previous feature-making step of THIS script made.",
                "Lines and arcs (0.11.0) are in the sketch's 2D frame like rect and circle; an arc's ccw is counter-clockwise IN THAT FRAME. Composites (polyline, slot, polygon, rounded rect, arc3, tangentArc) are expanded into lines and arcs before anything is drawn, and the result says what was drawn.",
                "A hole's depth is measured from the face, flat-bottomed (no drill point); a counterbore's or countersink's depth counts within it.",
                "A step whose feature comes back null is an ERROR that stops the script — it never reports success with nothing built."
            ]),
        );
    }
    v
}

// ── preferences ─────────────────────────────────────────────────────────────

pub fn get_preferences(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let target = resolve_document(ctx, args, "swx_get_preferences").ok();
    let call_args = target.as_ref().map(|t| t.args()).unwrap_or_else(|| json!({}));
    match ctx.call("get_preferences", call_args, 60) {
        Err(e) => sidecar_error(e),
        Ok(r) => ok_merge(r, json!({
            "_hint": "A DELIBERATELY SMALL set: only preferences swx_set_preference can write \
                      and read back, because a preference the bridge cannot verify is one it has \
                      no business claiming to support. Fusion's theme/navigation preferences have \
                      no SOLIDWORKS equivalent this bridge can drive — see the parity report. \
                      Note that geometry is always reported in SI or millimetres regardless of \
                      the document's display units.",
        })),
    }
}

pub fn set_preference(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    if arg_str(args, "name").is_none() || args.get("value").is_none() {
        return err(
            "name and value are required",
            "swx_get_preferences lists the names this bridge can write; `value` is the integer \
             enum value.",
        );
    }
    // H1 (round 5): this verb resolved NOTHING. With no `path` the com host fell
    // through to ActiveDoc and called SetUserPreferenceInteger on the user's
    // document, marking it dirty — a change to their model, entirely outside
    // the gate, which a later save would persist.
    let target = match resolve_document(ctx, args, "swx_set_preference") {
        Ok(t) => t,
        Err(e) => return e,
    };
    let grant = match crate::writes::authorize_in(ctx, args, "swx_set_preference", crate::writes::Kind::Preference, Some(&target), None) {
        Ok(g) => g,
        Err(e) => return e,
    };
    let mut call_args = crate::writes::with_grant(target.args(), &grant);
    if let Value::Object(m) = &mut call_args {
        m.insert("name".into(), args.get("name").cloned().unwrap_or(Value::Null));
        m.insert("value".into(), args.get("value").cloned().unwrap_or(Value::Null));
    }
    match ctx.call("set_preference", call_args, 60) {
        Err(e) => sidecar_error(e),
        Ok(r) => {
            crate::ledger::record("preference_set", json!({"name": r.get("name"), "value": r.get("value")}));
            ok_merge(r, json!({
                "_hint": "`verified` is the one field that matters: the bridge READS THE VALUE \
                          BACK after writing it, so verified:false means SOLIDWORKS silently \
                          refused. Document-scoped — it applies to the target document, not the \
                          application.",
            }))
        }
    }
}

// ── cloud ───────────────────────────────────────────────────────────────────

/// `swx_cloud_status` — the honest answer about 3DEXPERIENCE.
pub fn cloud_status(ctx: &Ctx, _args: &Value) -> Value {
    gate!(ctx);
    let detection = ctx.detection();
    let com = ctx.call("cloud_status", json!({}), 60).unwrap_or_else(|e| json!({"error": e.message}));
    ok_merge(com, json!({
        "edition": detection.edition(),
        "errorCode": "cloud_not_available",
        "supported": false,
        "solidworks": detection.to_json(),
        "_hint": "There is NO cloud file access in this bridge, and the reason is not laziness: \
                  the SOLIDWORKS COM API exposes no 3DSpace/3DDrive member at all (IsPLMConnected, \
                  GetPLMConnection, GetSearchFolders and EnumDocuments2 are absent or rejected on \
                  a live 3DEXPERIENCE R2026x install — `probedMembers` is that probe, run just \
                  now). Fusion's cloud verbs work because Autodesk publishes a REST API (APS) the \
                  bridge can authenticate against on its own; the 3DEXPERIENCE equivalent is \
                  3DSpace REST behind a tenant-specific CAS/passport login, which would be a \
                  separate OAuth story and is not built. Local files work fully: swx_find_files, \
                  swx_open, swx_save_as, swx_export.",
    }))
}

// ── per-format export aliases ───────────────────────────────────────────────

/// Formats verified to WRITE REAL BYTES from a part on a live SOLIDWORKS
/// Design Professional for Makers 2026 (3DEXPERIENCE R2026x), 2026-09-02.
pub const PROVEN_MODEL_FORMATS: &[&str] = &[
    "step", "stp", "iges", "igs", "stl", "parasolid", "x_t", "x_b", "sat", "3mf", "glb", "ply",
    "amf", "3dxml", "png", "jpg", "tif", "xaml", "pdf",
];

/// Formats SOLIDWORKS REFUSED for a part (swFileSaveError_e 256) on that same
/// install: DXF/DWG are drawing formats, OBJ and U3D are simply not available.
pub const REFUSED_FOR_PARTS: &[&str] = &["obj", "dxf", "dwg", "u3d"];

/// `.wrl` (VRML) raised a modal option sheet and HUNG the COM call until the
/// watchdog killed the child — so the bridge refuses it rather than wedging
/// someone's SOLIDWORKS for two minutes.
pub const BLOCKED_FORMATS: &[&str] = &["wrl", "vrml"];

/// Is this DESTINATION PATH one SOLIDWORKS must never be asked to write?
///
/// This is the check that matters, and the first version of it was wrong in a
/// way worth recording: v0.3's guard tested the `format` ARGUMENT, but
/// `SaveAs3` keys the format off the destination EXTENSION. So
/// `swx_export_step {outputPath:"x.wrl"}` sailed straight past a guard whose
/// entire purpose was to stop that call — and the per-format aliases hard-code
/// `format`, so for them the guard could never fire at all. The extension is
/// the only thing SOLIDWORKS actually looks at, so it is the only thing worth
/// checking. Enforced at the server AND again in the com host's `save_as3`,
/// because the modelling DSL reaches that function by a different road.
pub fn blocked_by_extension(path: &str) -> Option<&'static str> {
    normalized_extension(path).and_then(|ext| BLOCKED_FORMATS.iter().copied().find(|b| **b == *ext))
}

/// The extension Windows will ACTUALLY use, which is not what `Path::extension`
/// returns for a hostile string. Win32 canonicalisation strips trailing dots
/// and spaces, and an NTFS alternate-data-stream suffix is not part of the
/// name — so `a.wrl.`, `a.wrl ` and `a.wrl::$DATA` all land on `a.wrl`, and a
/// raw comparison waved all three through the guard on the format that hangs
/// SOLIDWORKS.
pub fn normalized_extension(path: &str) -> Option<String> {
    let last = path.rsplit(['/', '\\']).next().unwrap_or(path);
    // An ADS suffix (`name:stream`) is not part of the filename Windows opens.
    let last = last.split(':').next().unwrap_or(last);
    // Trailing dots and spaces are stripped by the OS, not honoured.
    let trimmed = last.trim_end_matches([' ', '.', '\t']);
    let dot = trimmed.rfind('.')?;
    if dot == 0 || dot + 1 >= trimmed.len() {
        return None;
    }
    Some(trimmed[dot + 1..].to_ascii_lowercase())
}

/// The refusal, shared by every path that can name a destination file.
pub fn blocked_path_error(path: &str, ext: &str) -> Value {
    err_code(
        "format_blocked",
        format!("{path} would write a .{ext} file, which this bridge refuses"),
        "VRML export raises a modal option sheet that no unattended caller can answer: on a live          install it hung the COM call until the watchdog killed the host, and left the dialog on          the user's screen. SOLIDWORKS picks the export format from the destination EXTENSION,          not from the `format` argument — so this is checked on the filename. Write STEP, 3MF or          GLB instead.",
    )
}

/// One export alias (`swx_export_step` and friends) — the same handler as
/// `swx_export`, with the format fixed, so the arg surface matches Fusion's
/// per-format verbs.
pub fn export_as(ctx: &Ctx, args: &Value, format: &'static str) -> Value {
    let mut a = args.clone();
    if let Value::Object(map) = &mut a {
        map.insert("format".into(), Value::from(format));
        // Fusion spells it outputPath; v0.2 spelled it out. Accept both.
        if let Some(p) = map.get("outputPath").cloned() {
            map.entry("out").or_insert(p);
        }
    }
    crate::verbs::dispatch(ctx, "swx_export", &a)
}

/// The extra guidance every export response carries in v0.3: which formats this
/// bridge has actually SEEN work, on a real install.
pub fn export_format_note() -> Value {
    json!({
        "provenModelFormats": PROVEN_MODEL_FORMATS,
        "refusedForParts": REFUSED_FOR_PARTS,
        "blockedFormats": BLOCKED_FORMATS,
        "note": "Verified on SOLIDWORKS Design Professional for Makers 2026 (3DEXPERIENCE \
                 R2026x) on 2026-09-02 by exporting a real solid and checking the bytes on \
                 disk. DXF and DWG are DRAWING formats — they are refused for a part and are \
                 expected to work from a .slddrw. VRML (.wrl) hangs SOLIDWORKS on a modal \
                 option sheet, so this bridge refuses it outright.",
    })
}

/// `swx_run_macro` — run a SOLIDWORKS VBA macro through
/// `ISldWorks::RunMacro2`.
///
/// SCAFFOLDING, and labelled as such: the COM signature is written from
/// Dassault's documentation and CodeStack's worked examples (which confirm both
/// that `.swp` is plain-text editable and that `RunMacro2` works from a
/// standalone executable), but it has NOT been run against a live SOLIDWORKS
/// yet. `docs/FUSION-PARITY-REPORT.md` grades it accordingly and the response
/// says so in its own `_hint`, because a verb that has never executed and a
/// verb that has are not the same thing and the report exists to keep that
/// distinction.
///
/// THE GATE. Every other write in this bridge is bounded by two questions — is
/// the document ours, is the destination clear — and a macro answers neither.
/// It is a VBA project with the whole SOLIDWORKS API in scope and the
/// filesystem behind it: it can model, save over anything, batch-export, delete
/// or quit, to documents this bridge has never seen. Nothing about the CALL
/// predicts that. So `Kind::RunMacro` is always gated and there is no safe case
/// carved out of it: running one requires the same opt-in chain as any write to
/// the user's data — the process started with `SWX_ALLOW_USER_WRITES=1`, plus
/// `allowUserWrites:true`, `confirm:true` and a `reason` on the call. In the
/// default posture this verb refuses, always. What is being authorized is
/// arbitrary code execution on somebody's desktop, and the only honest gate for
/// that is the human saying so each time.
pub fn run_macro(ctx: &Ctx, args: &Value) -> Value {
    gate!(ctx);
    let path = match arg_str(args, "path") {
        Some(p) => p.to_string(),
        None => {
            return err(
                "path is required",
                "The macro file: .swp (VBA project), .swb (older binary) or a compiled .dll. \
                 swx_find_files locates candidates. A bundled starting point ships with the \
                 bridge at resources/macros/adom-hello.swp — see the SKILL.",
            )
        }
    };
    // A macro is identified by module AND procedure. `RunMacro2` needs both,
    // and the SOLIDWORKS default for a recorded macro is `Main` in `main`.
    let module = arg_str(args, "module").unwrap_or("main").to_string();
    let procedure = arg_str(args, "procedure").or_else(|| arg_str(args, "proc")).unwrap_or("main").to_string();

    // ALWAYS GATED, with no carve-out, and gated FIRST. See
    // `writes::Kind::RunMacro`. Authorization precedes even the existence check
    // below: an unauthorized caller learning whether a given path exists on
    // somebody's disk is a small leak, but it is a leak through the one verb
    // whose whole point is that nothing about it is bounded — and the ordering
    // rule ("decide before you act, and before you look") is the same one whose
    // absence let swx_close_all_documents discard documents on its way to
    // returning a refusal.
    let grant = match crate::writes::authorize_in(
        ctx,
        args,
        "swx_run_macro",
        crate::writes::Kind::RunMacro,
        None,
        None,
    ) {
        Ok(g) => g,
        Err(e) => return e,
    };
    // The macro must EXIST before SOLIDWORKS is asked for it: RunMacro2 on a
    // missing path returns a bare error code, and "2" is not a useful answer to
    // a caller who mistyped a filename.
    if !std::path::Path::new(&path).exists() {
        return err_code(
            "macro_not_found",
            format!("no macro at {path}"),
            "Give the full path to the .swp/.swb/.dll. swx_find_files searches disk. The check \
             happens here because RunMacro2 answers a missing file with the same numeric code it \
             uses for a macro that failed to compile.",
        );
    }
    let job = ctx.jobs.start("run_macro", format!("Running {module}.{procedure}"), 60);
    let started = std::time::Instant::now();
    let mut call_args = crate::writes::with_grant(json!({}), &grant);
    if let Value::Object(m) = &mut call_args {
        m.insert("path".into(), Value::from(path.clone()));
        m.insert("module".into(), Value::from(module.clone()));
        m.insert("procedure".into(), Value::from(procedure.clone()));
    }
    match ctx.call("run_macro", call_args, 280) {
        Err(e) => sidecar_error(e),
        Ok(r) => {
            crate::ledger::record_time("swx_run_macro", started.elapsed().as_secs_f64());
            crate::ledger::record(
                "macro_run",
                json!({
                    "path": path,
                    "module": module,
                    "procedure": procedure,
                    "runError": r.get("runError"),
                    "reason": arg_str(args, "reason"),
                }),
            );
            ok_merge(r, json!({
                "progress": job.finished(),
                "windowEtiquette": "background",
                "_hint": "The macro ran inside SOLIDWORKS with the full API in scope — this \
                          bridge cannot tell you what it did. Check the result yourself: \
                          swx_docs for documents it opened or closed, swx_document_info and \
                          swx_get_parameters for changes to a model, swx_rebuild_check for \
                          errors it left behind. The macro is unloaded afterwards \
                          (swRunMacroUnloadAfterRun), so editing the .swp and running it again \
                          picks up the new version. NOT YET PROVEN ON A LIVE MACHINE: this verb \
                          is scaffolding written from the documented RunMacro2 signature — if it \
                          misbehaves, that is worth reporting.",
            }))
        }
    }
}