main
Drew Owens 0.11.1 source a674891 5d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641
//! THE GRACEFUL QUIT LADDER — `swx_stop` v2.
//!
//! Full design and evidence: `docs/QUIT-INVESTIGATION.md` §5. This module holds
//! the rules that must be decidable without a machine (so they are testable) and
//! the measurements that come from the machine rather than from a COM return
//! value (so they cannot lie).
//!
//! ## The three laws
//!
//! 1. **NEVER A KILL.** No rung reaches `taskkill`, `TerminateProcess`,
//!    `swx_kill`, or `RmForceShutdown`. `swx_kill` is a separate, differently
//!    gated verb; `swx_stop` must never call it, escalate to it, or recommend it
//!    in a hint. `RmForceShutdown` is not a runtime flag here — the Restart
//!    Manager rung passes a literal `0`.
//! 2. **NEVER ANSWER A DIALOG.** The census runs before every rung and again
//!    after each one. Any top-level dialog owned by the SOLIDWORKS pid stops the
//!    ladder and is REPORTED, never clicked, never Escaped, never `WM_CLOSE`d.
//!    On 2026-09-02 closing a stuck SOLIDWORKS modal through the window manager
//!    took the whole application down.
//! 3. **NOTHING IS DISOWNED UNTIL THE PROCESS IS VERIFIABLY GONE.** v1 cleared
//!    the created-document registry *before* it verified the quit, so a declined
//!    quit left the bridge unable to touch its own documents. Success is
//!    (pid + creation time) absent from a live census — never a COM call, never
//!    `GetActiveObject`.
//!
//! ## Why the reference release is load-bearing
//!
//! An automation client's outstanding `IDispatch` is exactly what keeps a
//! SOLIDWORKS alive (`UserControl` semantics, §1.2). v1 asked "are you still
//! there?" while holding a live reference for the whole match arm, so it could
//! not tell "declined" from "quit, but pinned by our own pointer". Worse, a
//! `WM_CLOSE` sent while we still hold one is the most direct way to manufacture
//! the half-closed `SLDWORKS.exe -Embedding` orphan this design exists to avoid.
//!
//! The refcount reasoning, stated once: `variant::Disp` wraps a `windows`-crate
//! `IDispatch`, which is a refcounted COM pointer — `Clone` calls `AddRef` and
//! `Drop` calls `Release`. The com host holds at most ONE cached reference
//! (`ComBackend::sw`) plus whatever locals a command creates. So "release
//! everything" is: drop every local `Disp` (end of scope) and set `self.sw =
//! None`. `release_com` does exactly that and then reports
//! `outstanding: 0`, which the ladder requires before any rung and before any
//! measurement. Re-attaching for a later measurement would defeat the point, so
//! the measurement never touches COM at all.

use crate::launchos::{ProcInfo, WindowInfo};
use serde_json::{json, Value};

/// `swCommands_App_Exit` — File ▸ Exit, through the app's own command
/// dispatcher. Source: SOLIDWORKS API `swCommands_e` (see
/// docs/QUIT-INVESTIGATION.md §1.5). Not inline anywhere.
pub const SW_COMMANDS_APP_EXIT: i32 = 2825;

/// The rungs, in order. There is no rung (g): the ladder ends in an honest
/// refusal, never in a kill.
pub const RUNGS: &[&str] = &["a", "b", "c", "d", "e", "f"];

/// How far the ladder may climb unless the caller says otherwise.
///
/// **"a" until the live gate has run.** Shipping the whole ladder armed on day
/// one would be exactly the mistake crash-reporter recovery avoided by shipping
/// `off`: rungs (b)-(f) send window messages and run VBA inside somebody's CAD
/// session, and not one of them has been observed on a real machine.
pub const DEFAULT_MAX_RUNG: &str = "a";

/// What each rung actually does, for the report and the ledger.
pub fn rung_action(rung: &str) -> &'static str {
    match rung {
        "a" => "UserControl=false; ExitApp; release every COM reference",
        "b" => "RunCommand(swCommands_App_Exit=2825)",
        "c" => "SendMessageTimeout(WM_CLOSE) to the main frame",
        "d" => "PostMessage(WM_SYSCOMMAND, SC_CLOSE) to the main frame",
        "e" => "RunMacro2 of the shipped two-line exit macro",
        "f" => "Restart Manager RmShutdown (RmForceShutdown OMITTED)",
        _ => "unknown rung",
    }
}

/// Per-rung budget in seconds, from §5.3.
pub fn rung_budget_secs(rung: &str) -> u64 {
    match rung {
        "a" | "b" => 30,
        "c" | "d" => 45,
        "e" => 60,
        "f" => 90,
        _ => 30,
    }
}

pub fn is_rung(r: &str) -> bool {
    RUNGS.contains(&r)
}

/// The rungs to try, in order, up to and including `max`. An unknown `max`
/// yields nothing — the caller refuses rather than guessing.
pub fn rungs_up_to(max: &str) -> Vec<&'static str> {
    match RUNGS.iter().position(|r| *r == max) {
        None => Vec::new(),
        Some(i) => RUNGS[..=i].to_vec(),
    }
}

/// The per-session children a GRACEFUL exit reaps. Their survival after the pid
/// is gone means the exit was abrupt, not clean.
pub const SESSION_CHILDREN: &[&str] =
    &["sldProcMon.exe", "sldsurfacing.exe", "sldimport.exe", "swCefSubProc.exe"];

/// The processes that MUST SURVIVE a graceful quit — by design (§2.6).
///
/// Their continued presence is CONSENT, not a failure: they are the platform's
/// own daemons and the user's licence/vault plumbing, none of which belongs to
/// the SOLIDWORKS session the bridge is ending. Reporting them as "not cleaned
/// up" would invite somebody to kill them, which is the launch path's oldest
/// lesson (`docs/LAUNCH-RESILIENCE.md` §3).
pub const EXPECTED_SURVIVORS: &[&str] = &[
    "ENOPLMCSAClient.exe",
    "EdmServerV6.exe",
    "SWXDesktopHelper.exe",
    "sldBgDwld.exe",
    "3DEXPERIENCELauncher.exe",
    "3DEXPERIENCELauncherSysTray.exe",
    "3DEXPERIENCELauncherService.exe",
];

/// Images whose behaviour at exit is undocumented: RECORD, DO NOT JUDGE (§5.4).
pub const UNJUDGED: &[&str] = &["ViewServerV6.exe", "CATSTART.exe", "CATSysDemon.exe"];

/// The identity of the session being quit, captured BEFORE the first rung (P7).
///
/// Same attribution law as the launch path: without a pid **and** its creation
/// time, "the process went away" cannot be told from "a pid was reused", and
/// every rung's success measurement depends on that distinction.
#[derive(Clone, Debug, PartialEq)]
pub struct Identity {
    pub pid: u32,
    pub created_at: u64,
    pub main_hwnd: Option<i64>,
}

impl Identity {
    pub fn to_json(&self) -> Value {
        json!({"processId": self.pid, "processCreatedAt": self.created_at, "mainHwnd": self.main_hwnd})
    }
}

/// IS THE PROCESS GONE? The one success criterion.
///
/// `None` means the census could not be read — which is a rung that FAILED to be
/// measured, never a rung that worked. A pid alone is not enough: a matching pid
/// with a DIFFERENT creation time is a new process, and the session we were
/// quitting is gone.
pub fn process_gone(census: Result<&Vec<ProcInfo>, &String>, id: &Identity) -> Option<bool> {
    let c = census.ok()?;
    Some(!c.iter().any(|p| p.pid == id.pid && p.created_at == Some(id.created_at)))
}

/// EVERY PID A BLOCKING DIALOG COULD BELONG TO: SOLIDWORKS itself, plus the CEF
/// (browser) subprocesses it started.
///
/// The 3DEXPERIENCE sign-in, licence and cloud-save windows are web views, and a
/// web view's window belongs to `swCefSubProc.exe` — a CHILD of `sldworks.exe`,
/// with its own pid. A census of the SOLIDWORKS pid alone cannot see them, so
/// "no dialog is on screen" was provable while the platform was holding a modal
/// asking the user to sign in. Attribution is by parentage, never by class name:
/// only a `swCefSubProc.exe` whose ppid IS this session's pid counts, so another
/// SOLIDWORKS session's web view is never mistaken for ours.
///
/// `None` when the census could not be read — which makes the dialog question
/// UNKNOWN, and UNKNOWN refuses.
pub fn dialog_census_pids(census: Result<&Vec<ProcInfo>, &String>, id: &Identity) -> Option<Vec<u32>> {
    let c = census.ok()?;
    let mut pids = vec![id.pid];
    for p in c.iter() {
        if p.image.eq_ignore_ascii_case("swCefSubProc.exe") && p.ppid == Some(id.pid) {
            pids.push(p.pid);
        }
    }
    Some(pids)
}

/// The per-session children still alive, and the expected survivors still alive.
pub fn reap_report(census: &[ProcInfo]) -> (Vec<String>, Vec<String>, Vec<String>) {
    let names = |set: &[&str]| -> Vec<String> {
        census
            .iter()
            .filter(|p| set.iter().any(|s| s.eq_ignore_ascii_case(&p.image)))
            .map(|p| p.image.clone())
            .collect()
    };
    (names(SESSION_CHILDREN), names(EXPECTED_SURVIVORS), names(UNJUDGED))
}

/// IS THIS HWND STILL THE WINDOW WE MEASURED? Asked immediately before a message
/// rung sends anything.
///
/// Window handles are recycled. Rungs (c) and (d) send to an hwnd captured at
/// precondition time, and between then and the send SOLIDWORKS may have gone
/// (rung (a) is the rung before them, and its whole purpose is to make it go) —
/// at which point that handle can belong to somebody else's window, and
/// `WM_CLOSE` to somebody else's window is a close nobody asked for. Both facts
/// must answer: the window's owning pid, and that pid's creation time.
pub fn hwnd_still_owned(
    hwnd_pid: Option<u32>,
    hwnd_pid_created_at: Option<u64>,
    id: &Identity,
) -> bool {
    hwnd_pid == Some(id.pid) && hwnd_pid_created_at == Some(id.created_at)
}

/// A dialog that stops the ladder, classified — and NEVER answered.
#[derive(Clone, Debug, PartialEq)]
pub struct BlockingDialog {
    pub hwnd: i64,
    pub title: String,
    pub kind: &'static str,
    /// WHAT IT SAYS AND WHAT IT OFFERS. `None` = not read, or unreadable —
    /// UNKNOWN, never "it said nothing".
    pub contents: Option<crate::launchos::DialogContents>,
    /// The window as measured — class, owner, whether the owner is enabled,
    /// styles, rect — so a window nobody recognises can be judged from the reply.
    pub window: Option<WindowInfo>,
    /// Does it block? Decided by `judge_window` from the evidence, with the reason.
    pub blocking: bool,
    pub why: String,
    /// THE OWNER-STATE SETTLE (0.9.8): when ownerEnabled was the ONLY fact that
    /// made this window blocking, the re-reads of its owner's enabled state —
    /// `{reads:[{atMs, enabled}], ownerDisabledTransiently}`. `None` = no settle
    /// was needed or allowed.
    pub owner_settle: Option<Value>,
}

/// The captions a custom-drawn window's dialog TEMPLATE carries — measured
/// 2026-10-01 on the 3DEXPERIENCE "Connected to …" notice, whose Static controls
/// read "Title", "Description", "Button1", "Button2", "Hyperlink Text 1",
/// "Hyperlink Text 2" while the window SHOWED something else entirely. When every
/// readable caption is one of these, the text is not the window's words.
pub const TEMPLATE_PLACEHOLDERS: &[&str] =
    &["Title", "Description", "Button1", "Button2", "Hyperlink Text 1", "Hyperlink Text 2"];

pub fn text_is_template_placeholder(c: &crate::launchos::DialogContents) -> bool {
    let all: Vec<&String> = c.text.iter().chain(c.buttons.iter()).collect();
    !all.is_empty() && all.iter().all(|t| TEMPLATE_PLACEHOLDERS.contains(&t.trim()))
}

/// DOES THIS WINDOW BLOCK? Judged by MEASURED MODALITY, not by "a window exists"
/// (0.9.4). Measured 2026-10-01: a title-less `#32770` (hwnd 331370) stopped
/// every quit for minutes — and it was the 3DEXPERIENCE "Connected to …"
/// NOTIFICATION: owner frame ENABLED for all 20 reads over 95 s, WS_EX_TOOLWINDOW,
/// no Button-class child, custom-drawn. A window BLOCKS when:
///   * its TITLE or its TEXT says it is a save prompt, a recovery or an import
///     diagnostics prompt (never overridden by any evidence); or
///   * its title says another known blocking kind (update, settings, message,
///     export options) — a licence-type title alone is the exception (0.9.5,
///     adversary pass 43): measured notification evidence overrides it, because
///     the Connected notice with a title would block every quit and the real
///     License Manager blocks on its Button-class OK anyway; or
///   * its owner frame is DISABLED — a real modal; or
///   * it has a Button-class child (hidden ones count) — something on offer; or
///   * it shows TWO OR MORE button-like captions (Save, Don't Save, Cancel, Yes,
///     No, OK, Retry, …) in any control class, visible or hidden — SOLIDWORKS
///     draws many controls itself. The measured template placeholders
///     ("Button1", "Hyperlink Text 1", "Title", …) are not button-like.
/// It is a `notification` — reported, never answered, and NOT a reason to refuse
/// a quit, a sweep or an import — only when ALL of these were MEASURED: it is a
/// tool window, its owner frame is enabled, its controls were read, none is a
/// Button and fewer than two captions offer a choice. Anything unread FAILS
/// CLOSED: blocking.
///
/// OPEN GAPS that pre-date 0.9.4 (named by adversary pass 43): a modal whose
/// class is not `#32770` is never judged at all, and a prompt drawn INSIDE the
/// main frame (PropertyManager, task pane) is a child window the census never
/// sees. What bounds the damage there: a sweep saves every document before it
/// closes anything, and swx_stop requires zero unsaved documents.
pub fn judge_window(
    w: &WindowInfo,
    contents: Option<&crate::launchos::DialogContents>,
) -> (bool, &'static str, String) {
    let title_kind = crate::abwin::classify(&w.title).0;
    // ONE CLASSIFIER for the title AND the words (adversary passes 43 and 44):
    // the same function swx_dismiss_blocking_dialogs uses before it closes.
    let (words_kind, source, _, _) = classify_words(&w.title, contents);
    // A save, recovery or import prompt blocks whatever the evidence says — by
    // title OR by text. No measured modality overrides these.
    if NEVER_OVERRIDDEN_KINDS.contains(&words_kind) {
        return (true, words_kind, format!("a known blocking kind ({words_kind}) by its {source}"));
    }
    // Any other title kind blocks by title — EXCEPT a licence-type title, which
    // measured notification evidence may override (W2): the 3DEXPERIENCE
    // "Connected" notice with a title would otherwise block every quit again,
    // and the real License Manager blocks on its Button-class OK regardless.
    if title_kind != "unknown" && title_kind != "license" {
        return (true, title_kind, format!("a known blocking kind ({title_kind}) by its title"));
    }
    let kind = title_kind;
    match w.owner_enabled {
        Some(false) => return (true, kind, "its owner frame is DISABLED: a modal".into()),
        None => return (true, kind, "whether its owner frame is enabled could not be read".into()),
        Some(true) => {}
    }
    let Some(c) = contents else {
        return (true, kind, "its controls could not be read".into());
    };
    if c.has_button_class_child() {
        return (true, kind, "it has a Button-class control: something on offer".into());
    }
    let offered = button_like_captions(c);
    if offered.len() >= 2 {
        return (
            true,
            kind,
            format!(
                "it shows {} button-like captions ({}) — something is on offer, whatever class \
                 draws them",
                offered.len(),
                offered.join(", ")
            ),
        );
    }
    match w.exstyle {
        Some(x) if x & crate::launchos::WS_EX_TOOLWINDOW != 0 => (
            false,
            "notification",
            if kind == "license" {
                "a licence-type TITLE, but measured notification evidence overrides a title-only \
                 guess: a tool window whose owner frame is ENABLED, controls read, no Button-class \
                 control and no button-like captions"
                    .into()
            } else {
                "a tool window whose owner frame is ENABLED and which has no Button-class control \
                 and no button-like captions: a notification, not a modal"
                    .into()
            },
        ),
        Some(_) => (true, kind, "not a tool window: not provably a notification".into()),
        None => (true, kind, "its extended style could not be read".into()),
    }
}

/// THE ONE CLASSIFIER over a window's TITLE and its WORDS (text and button
/// captions): `(kind, source, blocking, neverAutoAnswer)`. A save, recovery or
/// import kind from EITHER wins; otherwise the title's kind; otherwise the first
/// kind the words name; otherwise `unknown` (never auto-answered). `source` is
/// "title", "text" or "none". Used by `judge_window` and by
/// swx_dismiss_blocking_dialogs, so a save question in a window titled
/// "Warning" is a save question to both.
pub fn classify_words(
    title: &str,
    contents: Option<&crate::launchos::DialogContents>,
) -> (&'static str, &'static str, bool, bool) {
    let t = crate::abwin::classify(title);
    let words: Vec<(&'static str, bool, bool)> = contents
        .map(|c| {
            c.text
                .iter()
                .chain(c.buttons.iter())
                .map(|x| crate::abwin::classify(x))
                .filter(|k| k.0 != "unknown")
                .collect()
        })
        .unwrap_or_default();
    if NEVER_OVERRIDDEN_KINDS.contains(&t.0) {
        return (t.0, "title", t.1, t.2);
    }
    if let Some(k) = words.iter().find(|k| NEVER_OVERRIDDEN_KINDS.contains(&k.0)) {
        return (k.0, "text", k.1, k.2);
    }
    if t.0 != "unknown" {
        return (t.0, "title", t.1, t.2);
    }
    if let Some(k) = words.first() {
        return (k.0, "text", k.1, k.2);
    }
    (t.0, "none", t.1, t.2)
}

/// Kinds that block whatever the measured evidence says — a choice about the
/// user's work (save), their recovered work (recovery), or the geometry an
/// import produces.
pub const NEVER_OVERRIDDEN_KINDS: &[&str] = &["save_changes", "document_recovery", "import_diagnostics"];

/// Captions that offer a choice. Matched whole, case-insensitive, with `&`
/// accelerators and trailing dots removed. The measured TEMPLATE PLACEHOLDERS
/// ("Button1", "Button2", "Hyperlink Text 1/2", "Title", "Description") are NOT
/// button-like: they are what the Connected notice's template left behind.
pub const BUTTON_LIKE: &[&str] = &[
    "save", "don't save", "do not save", "dont save", "save all", "cancel", "yes", "no", "ok",
    "retry", "abort", "ignore", "continue", "close", "discard", "yes to all", "no to all",
];

/// The button-like captions among a window's text and buttons, any control
/// class, visible or hidden (the text read includes hidden controls).
pub fn button_like_captions(c: &crate::launchos::DialogContents) -> Vec<String> {
    let mut out: Vec<String> = Vec::new();
    for t in c.text.iter().chain(c.buttons.iter()) {
        let n = t
            .replace('&', "")
            .replace('\u{2019}', "'")
            .trim()
            .trim_end_matches(|ch| ch == '.' || ch == '\u{2026}')
            .trim()
            .to_lowercase();
        if BUTTON_LIKE.contains(&n.as_str()) && !out.iter().any(|o| o.to_lowercase() == n) {
            out.push(t.trim().to_string());
        }
    }
    out
}

/// THE DIALOG CENSUS, JUDGED: every visible `#32770`, its contents read, each
/// judged. `blocking` is the first that blocks; `notifications` are the ones
/// that do not — reported, never answered.
pub struct DialogCensus {
    pub blocking: Option<BlockingDialog>,
    pub notifications: Vec<BlockingDialog>,
    /// EVERY OTHER blocking dialog (0.11.1): `blocking` is the first, which
    /// decides a refusal; a census that reports never drops the rest.
    pub more_blocking: Vec<BlockingDialog>,
}

impl DialogCensus {
    /// Every blocking dialog, the first first.
    pub fn all_blocking(&self) -> Vec<&BlockingDialog> {
        self.blocking.iter().chain(self.more_blocking.iter()).collect()
    }
}

/// How many times, and how far apart, a disabled owner is re-read before a
/// window that is a notification on every other fact is called a modal.
pub const OWNER_SETTLE_READS: u32 = 5;
fn owner_settle_ms() -> u64 {
    std::env::var("SWX_OWNER_SETTLE_MS").ok().and_then(|v| v.trim().parse().ok()).unwrap_or(400)
}

/// MODALITY IS A PERSISTENT MEASUREMENT, NOT ONE READ (0.9.8). MEASURED
/// 2026-10-01 on 0.9.7: the 3DEXPERIENCE Connected notice (same class, styles,
/// size and placeholder captions as the morning's) was read with its owner frame
/// DISABLED — at an instant when SOLIDWORKS was busy inside the sweep's own
/// close/save — and stopped the sweep as a modal; seconds later the frame was
/// enabled again. So when a window is a notification on EVERY other measured
/// fact (tool window, controls read, no Button class, no button-like captions,
/// no save/recovery/import words) and ONLY its owner's enabled state says
/// blocking, the owner is re-read up to OWNER_SETTLE_READS times ~400 ms apart:
/// disabled (or unreadable) on every read -> blocking, as before; enabled on a
/// later read -> a notification, reported with `ownerDisabledTransiently:true`
/// and the read history. The settle is never applied to a window with a
/// Button-class child, button-like captions, or a save/recovery/import kind —
/// `judge_window` with the owner taken as enabled still blocks those. The census
/// runs between this bridge's own COM calls: no call of the sweep is in flight
/// while it reads.
fn settle_owner(w: &WindowInfo, contents: Option<&crate::launchos::DialogContents>, os: &dyn crate::launchos::LaunchOs) -> Option<(bool, Value)> {
    if w.owner_enabled != Some(false) {
        return None;
    }
    let mut assumed = w.clone();
    assumed.owner_enabled = Some(true);
    if judge_window(&assumed, contents).0 {
        return None; // something other than the owner's state blocks it
    }
    let mut reads = vec![json!({"atMs": crate::launchos::epoch_ms() as u64, "enabled": false, "read": "census"})];
    for _ in 0..OWNER_SETTLE_READS {
        std::thread::sleep(std::time::Duration::from_millis(owner_settle_ms()));
        let e = os.owner_enabled_of(w.hwnd);
        reads.push(json!({"atMs": crate::launchos::epoch_ms() as u64, "enabled": e}));
        if e == Some(true) {
            return Some((true, json!({"reads": reads, "ownerDisabledTransiently": true})));
        }
    }
    Some((false, json!({"reads": reads, "ownerDisabledTransiently": false})))
}

pub fn dialog_census(windows: &[WindowInfo], os: &dyn crate::launchos::LaunchOs) -> DialogCensus {
    let mut out = DialogCensus { blocking: None, notifications: Vec::new(), more_blocking: Vec::new() };
    for w in windows.iter().filter(|w| w.is_dialog() && w.visible) {
        let contents = os.dialog_contents(w.hwnd).ok();
        // A body that could not be read is judged as unread (0.9.12).
        let readable = contents.as_ref().filter(|c| !c.body_unreadable());
        let (mut blocking, mut kind, mut why) = judge_window(w, readable);
        let mut owner_settle = None;
        if blocking {
            if let Some((enabled_later, history)) = settle_owner(w, readable, os) {
                if enabled_later {
                    let mut assumed = w.clone();
                    assumed.owner_enabled = Some(true);
                    let (b, k, y) = judge_window(&assumed, readable);
                    blocking = b;
                    kind = k;
                    why = format!(
                        "{y} — its owner frame read DISABLED at first and ENABLED on a re-read: \
                         SOLIDWORKS was busy, not held by this window (ownerDisabledTransiently)"
                    );
                } else {
                    why = format!(
                        "{why} — on every one of {} re-reads over the settle window",
                        OWNER_SETTLE_READS
                    );
                }
                owner_settle = Some(history);
            }
        }
        let d = BlockingDialog {
            hwnd: w.hwnd,
            title: w.title.clone(),
            kind,
            contents,
            window: Some(w.clone()),
            blocking,
            why,
            owner_settle,
        };
        if blocking {
            if out.blocking.is_none() {
                out.blocking = Some(d);
            } else {
                out.more_blocking.push(d);
            }
        } else {
            out.notifications.push(d);
        }
    }
    out
}

impl BlockingDialog {
    pub fn to_json(&self) -> Value {
        let c = self.contents.as_ref();
        // A TEMPLATE'S CAPTIONS ARE NOT THE WINDOW'S WORDS (measured 2026-10-01).
        let placeholder = c.map(text_is_template_placeholder).unwrap_or(false);
        let body_unreadable = c.map(|c| c.body_unreadable()).unwrap_or(false);
        json!({
            "hwnd": self.hwnd,
            "title": self.title,
            "kind": self.kind,
            "blocking": self.blocking,
            "why": self.why,
            "textIsTemplatePlaceholder": placeholder,
            "placeholderCaptions": if placeholder { c.map(|c| json!(c.text)).unwrap_or(Value::Null) } else { Value::Null },
            "evidence": {
                "window": self.window.as_ref().map(|w| w.to_json()),
                "controls": c.map(|c| c.to_json()["controls"].clone()),
            },
            // A TITLE IS NOT THE MESSAGE. Live 2026-09-17 the sweep stopped on
            // `{"kind":"license","title":"License Manager"}` — correct, and
            // unactionable: the window actually said the licence SERVER was
            // unreachable and the session would end after five more failed
            // attempts. That is a countdown somebody needs to see.
            "text": if placeholder || body_unreadable { Value::Null } else { c.map(|c| json!(c.text)).unwrap_or(Value::Null) },
            "buttons": if body_unreadable { Value::Null } else { c.map(|c| json!(c.buttons)).unwrap_or(Value::Null) },
            // HONEST (0.9.12): a DirectUIHWND task dialog's words are not in
            // child controls, so a walk that read none of them did not read it.
            "contentsReadable": c.is_some() && !body_unreadable,
            "contentsUnreadableWhy": if body_unreadable {
                json!("its body is a DirectUIHWND task dialog (or has no child controls): its words are not \
                       in child controls this bridge can read — take a screenshot to see them")
            } else if c.is_none() { json!("the controls could not be read") } else { Value::Null },
            // Whether the only thing on offer is acknowledgement. This bridge
            // still NEVER presses it — see `never_answer` — and whether it ever
            // may is a question for the user, not an inference from a control
            // tree.
            "acknowledgeOnly": c.map(|c| json!(c.acknowledge_only())).unwrap_or(Value::Null),
            "ownerDisabledTransiently": self.owner_settle.as_ref().and_then(|o| o.get("ownerDisabledTransiently").cloned()).unwrap_or(json!(false)),
            "ownerReads": self.owner_settle.as_ref().and_then(|o| o.get("reads").cloned()).unwrap_or(Value::Null),
            "neverAnswer": true,
            // WHICH WINDOW, AND WHERE IT CAME FROM (0.11.1): an operator facing a
            // refusal sees exactly which dialog, whose it is, how long it has been
            // up, and whether a bridge verb raised it.
            "className": self.window.as_ref().map(|w| w.class_name.clone()),
            "owner": self.window.as_ref().and_then(|w| w.owner),
            "rect": self.window.as_ref().and_then(|w| w.rect),
            "visible": self.window.as_ref().map(|w| w.visible),
            "attribution": crate::dialogs::attribution(self.hwnd, crate::launchos::os().as_ref()),
        })
    }

    /// `to_json`, plus what the bridge may do about it: the standard file
    /// dialog it provably raised, with an empty name, is the one it may cancel.
    pub fn report(&self, os: &dyn crate::launchos::LaunchOs) -> Value {
        let mut v = self.to_json();
        let fd = os.file_dialog(self.hwnd).ok();
        v["fileDialog"] = match &fd {
            Some(f) => json!({"standard": f.standard, "fileNameEmpty": f.file_name.as_ref().map(|n| n.trim().is_empty())}),
            None => Value::Null,
        };
        match crate::dialogs::may_cancel(self.hwnd, os) {
            Ok((verb, _)) => {
                v["bridgeMayCancel"] = json!(true);
                v["cancelBecause"] = json!(format!(
                    "the standard file dialog with an EMPTY file name, raised during {verb} with no user input since:                      swx_dismiss_blocking_dialogs {{close:true, confirm:true, reason}} cancels it (Cancel changes nothing)"
                ));
            }
            Err(why) => {
                v["bridgeMayCancel"] = json!(false);
                v["notCancellableBecause"] = json!(why);
                v["whoAnswers"] = json!("the user cancels it");
            }
        }
        v
    }

    /// READ WHAT IT SAYS. Read-only; a failure leaves `contents` as `None`.
    pub fn read_contents(&mut self, os: &dyn crate::launchos::LaunchOs) {
        self.contents = os.dialog_contents(self.hwnd).ok();
    }
    /// The error code this dialog produces. A save prompt is called out
    /// separately because at a VERIFIED zero documents it should be impossible —
    /// its appearance means our document count was wrong, which is a bug to
    /// report, not a dialog to dismiss.
    pub fn error_code(&self) -> &'static str {
        match self.kind {
            "save_changes" => "stop_blocked_by_save_prompt",
            _ => "stop_blocked_by_dialog",
        }
    }
}

/// The first blocking dialog in a window census, if any.
///
/// Only top-level `#32770` windows owned by the SOLIDWORKS pid count — the
/// census is already scoped to that pid, and the main frame is an `Afx:` class,
/// never a dialog.
pub fn first_blocking_dialog(windows: &[WindowInfo]) -> Option<BlockingDialog> {
    windows.iter().filter(|w| w.is_dialog() && w.visible).map(|w| {
        let (kind, _blocking, _never) = crate::abwin::classify(&w.title);
        BlockingDialog {
            hwnd: w.hwnd,
            title: w.title.clone(),
            kind,
            contents: None,
            window: Some(w.clone()),
            blocking: true,
            why: "a visible dialog, not yet judged".into(),
            owner_settle: None,
        }
    }).next()
}

/// What a rung reported doing. `Exited` is never trusted on its own — the census
/// is what decides — but a rung that says it was blocked stops the ladder,
/// because an access-denied is a fact about the machine's integrity levels, not
/// about SOLIDWORKS' willingness to quit.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RungResult {
    /// The rung ran and the process is gone (census-confirmed).
    Quit,
    /// The rung ran and the process is still there.
    Declined,
    /// A top-level dialog appeared. LADDER STOPS.
    DialogAppeared,
    /// UIPI refused the message. LADDER STOPS — escalating past an
    /// access-denied would just try the same blocked gesture another way.
    UipiBlocked,
    /// The rung could not be attempted (not shipped, not permitted, no hwnd).
    Skipped,
    /// The rung ran and its effect could not be MEASURED. A rung that cannot be
    /// measured is a rung that failed.
    Unmeasured,
}

impl RungResult {
    pub fn name(&self) -> &'static str {
        match self {
            RungResult::Quit => "quit",
            RungResult::Declined => "declined",
            RungResult::DialogAppeared => "dialogAppeared",
            RungResult::UipiBlocked => "uipiBlocked",
            RungResult::Skipped => "skipped",
            RungResult::Unmeasured => "unmeasured",
        }
    }
    /// Does the ladder stop here, whatever the reason?
    pub fn stops_ladder(&self) -> bool {
        matches!(
            self,
            RungResult::Quit | RungResult::DialogAppeared | RungResult::UipiBlocked
        )
    }
}

/// Map the host's own word for what happened onto a result. An unrecognised
/// answer is `Unmeasured`, not `Declined`: "I do not understand the reply" is not
/// "it refused".
pub fn result_from_host(word: &str) -> RungResult {
    match word {
        "exited" | "quit" => RungResult::Quit,
        "declined" => RungResult::Declined,
        "dialogAppeared" | "dialog" => RungResult::DialogAppeared,
        "uipiBlocked" => RungResult::UipiBlocked,
        "skipped" => RungResult::Skipped,
        _ => RungResult::Unmeasured,
    }
}

/// One rung's full record.
#[derive(Clone, Debug)]
pub struct RungTry {
    pub rung: &'static str,
    pub action: &'static str,
    pub result: RungResult,
    pub process_gone: Option<bool>,
    pub elapsed_sec: f64,
    pub new_windows: usize,
    pub dialog: Option<BlockingDialog>,
    pub children_alive: Vec<String>,
    pub survivors_alive: Vec<String>,
    pub unjudged_alive: Vec<String>,
    /// Whether the census those three lists came from could be READ. Three empty
    /// lists otherwise say "everything was reaped, nothing survived" — the most
    /// reassuring possible reading of no information at all.
    pub census_readable: bool,
    pub detail: Option<String>,
    pub host_error: Option<String>,
    /// This attempt followed PRIMING a never-had-a-document session (one
    /// bridge-owned scratch part created and closed) — measured 2026-10-01.
    pub primed: bool,
}

impl RungTry {
    pub fn to_json(&self) -> Value {
        json!({
            "rung": self.rung,
            "action": self.action,
            "result": self.result.name(),
            // `null` when the census could not be read — a rung that cannot be
            // measured is a rung that failed, and it says so rather than
            // reporting a comfortable false.
            "processGone": self.process_gone,
            "elapsedSec": (self.elapsed_sec * 10.0).round() / 10.0,
            "newWindows": self.new_windows,
            "dialog": self.dialog.as_ref().map(|d| d.to_json()),
            "sessionChildrenStillAlive": self.children_alive,
            "expectedSurvivorsAlive": self.survivors_alive,
            "undocumentedStillAlive": self.unjudged_alive,
            // The three lists above are only evidence if this is true.
            "censusReadable": self.census_readable,
            "detail": self.detail,
            "hostError": self.host_error,
            "primed": self.primed,
        })
    }
}

/// Why a quit was refused before any rung ran. Every variant carries a code a
/// caller can branch on, and the UNKNOWN ones are deliberately one code: the
/// caller's move is the same for all of them, and it is never "proceed".
#[derive(Clone, Debug, PartialEq)]
pub enum Refusal {
    /// A precondition could not be MEASURED. Never proceed.
    Unknown { what: &'static str, detail: String },
    /// A precondition was measured and is false.
    Known { code: &'static str, message: String },
}

impl Refusal {
    pub fn code(&self) -> &'static str {
        match self {
            Refusal::Unknown { .. } => "stop_preconditions_unknown",
            Refusal::Known { code, .. } => code,
        }
    }
    pub fn message(&self) -> String {
        match self {
            Refusal::Unknown { what, detail } => format!(
                "cannot verify {what} before quitting SOLIDWORKS, so this bridge will not try: \
                 {detail}"
            ),
            Refusal::Known { message, .. } => message.clone(),
        }
    }
}

/// THE PRECONDITION SWEEP, as a pure decision over already-measured facts.
///
/// Every one is required and every one is re-measured immediately before each
/// rung — not inherited from the caller, not cached from an earlier verb. An
/// UNKNOWN in any of them refuses; `force` waives **P2 only**.
///
/// `force` deliberately does NOT waive P1, P3, P4, P5 or P7: those are not
/// questions about the user's data, they are the difference between a quit and a
/// wedge. v1's `force` waived the whole front gate, and v1 gated on the UNSAVED
/// count — so on 2026-09-08, with five of Drew's clean documents open and
/// `unsavedDocuments:0`, a bare `swx_stop` would have sailed past the guard and
/// tried to quit a session full of his work.
#[allow(clippy::too_many_arguments)]
/// DOES THE SESSION WE ARE ABOUT TO END MATCH THE ONE THE LAUNCH RECORD CLAIMS?
///
/// Belt and braces, added after a re-attribution bug labelled a session
/// `attributed:true` while COM was attached to a DIFFERENT process: the ladder
/// builds its `Identity` from the COM pid, and a wrong attribution record would
/// then have authorised ending a process nothing had proved was ours.
///
/// `claimed` is the launch's own record, `identity` is what COM answers for now.
/// A mismatch refuses. No claim at all (`None`) is not a mismatch — it is simply
/// a check with nothing to compare, and the other preconditions still stand.
pub fn identity_matches_attribution(
    claimed: Option<(u32, u64)>,
    identity: Option<&Identity>,
) -> Result<(), Refusal> {
    let (Some((pid, created)), Some(id)) = (claimed, identity) else { return Ok(()) };
    if id.pid == pid && id.created_at == created {
        return Ok(());
    }
    Err(Refusal::Known {
        code: "identity_mismatch",
        message: format!(
            "this bridge's launch record claims SOLIDWORKS pid {pid} (created {created}), but COM \
             is attached to pid {} (created {}) — so the session about to be ended is NOT the one \
             the attribution was proved for, and nothing here may act on that difference",
            id.pid, id.created_at
        ),
    })
}

pub fn check_preconditions(
    open_docs: Option<u64>,
    listed_documents: Option<u64>,
    docs_truncated: bool,
    unsaved: Option<u64>,
    dialog: Option<&BlockingDialog>,
    dialog_census_readable: bool,
    busy: Option<bool>,
    host_busy: bool,
    responding: Option<bool>,
    identity: Option<&Identity>,
    launch_in_flight: bool,
    force: bool,
) -> Result<(), Refusal> {
    // ORDER MATTERS. The checks that do NOT depend on being able to identify the
    // process come first, because a fact that is DEFINITELY TRUE is more useful
    // to a caller than an unknown: "six documents are open" is actionable, while
    // "I could not read the pid" masks it. Every one of these still refuses.
    //
    // P1 — the OPEN count, and only from a count the enumeration agreed with.
    if docs_truncated {
        return Err(Refusal::Unknown {
            what: "how many documents are open",
            detail: "the document set is too large to enumerate (likely leaked import temps), so \
                     an empty list would mean 'unlistable', not 'none open'. Run \
                     swx_close_import_temps, then retry."
                .into(),
        });
    }
    match open_docs {
        None => {
            return Err(Refusal::Unknown {
                what: "how many documents are open",
                detail: "GetDocumentCount did not answer. Quitting with the user's documents open \
                         is the one outcome this verb exists to prevent."
                    .into(),
            })
        }
        Some(0) => {}
        Some(n) => {
            return Err(Refusal::Known {
                code: "open_documents_present",
                message: format!(
                    "{n} document(s) are open in the user's SOLIDWORKS — this bridge quits only a \
                     session with NONE open, and `force` does not waive that"
                ),
            })
        }
    }
    // P1b — THE COUNT MUST AGREE WITH THE ENUMERATION. `GetDocumentCount` and
    // `GetDocuments` are two different COM reads, and the Python gate has always
    // cross-checked them before it would let a live run proceed; the verb did
    // not. When they disagree neither is evidence, and "zero" is precisely the
    // answer we are least entitled to assume. (Above the cap the enumeration is
    // deliberately empty — that case is already refused above as truncated.)
    match (open_docs, listed_documents) {
        (Some(c), Some(l)) if c != l => {
            return Err(Refusal::Unknown {
                what: "how many documents are open",
                detail: format!(
                    "GetDocumentCount says {c} and the enumeration lists {l} — they DISAGREE, so \
                     neither is evidence and 'none open' is not established"
                ),
            })
        }
        _ => {}
    }
    // P2 — the unsaved count. The one thing `force` may waive.
    if !force {
        match unsaved {
            None => {
                return Err(Refusal::Unknown {
                    what: "whether any open document has unsaved changes",
                    detail: "GetSaveFlag did not answer for every document. An unanswerable \
                             question is not permission."
                        .into(),
                })
            }
            Some(0) => {}
            Some(n) => {
                return Err(Refusal::Known {
                    code: "unsaved_documents",
                    message: format!("{n} open document(s) have unsaved changes"),
                })
            }
        }
    }
    // P7 — the identity. Everything below this line is measured against a
    // specific process, and without a pid AND its creation time "the process
    // went away" cannot be told from "a pid was reused".
    if identity.is_none() {
        return Err(Refusal::Unknown {
            what: "which SOLIDWORKS process this is (pid + creation time + main window)",
            detail: "GetProcessID / the process census / GetHWndx64 did not all answer. Every \
                     rung's success measurement is 'this exact process is gone', which a pid \
                     without a creation time cannot express."
                .into(),
        });
    }
    // P3 — no blocking dialog, from a census we could actually take.
    if !dialog_census_readable {
        return Err(Refusal::Unknown {
            what: "whether a dialog is on screen",
            detail: "the window census could not be taken. A quit sent at a modal is a different \
                     act with a different outcome — on 2026-09-02 closing a stuck SOLIDWORKS \
                     modal took the whole application down."
                .into(),
        });
    }
    if let Some(d) = dialog {
        return Err(Refusal::Known {
            code: d.error_code(),
            message: format!(
                "SOLIDWORKS is showing a dialog ({}): \"{}\" (hwnd {}, class {}, owned by {}) — this bridge never answers a \
                 dialog, so the quit stops here",
                d.kind,
                d.title,
                d.hwnd,
                d.window.as_ref().map(|w| w.class_name.as_str()).unwrap_or("?"),
                d.window.as_ref().and_then(|w| w.owner).map(|o| o.to_string()).unwrap_or_else(|| "no window".into())
            ),
        });
    }
    // P4 — not busy, on both sides.
    match busy {
        None => {
            return Err(Refusal::Unknown {
                what: "whether SOLIDWORKS is mid-command",
                detail: "CommandInProgress did not answer.".into(),
            })
        }
        Some(true) => {
            return Err(Refusal::Known {
                code: "host_app_busy",
                message: "SOLIDWORKS is in the middle of a command".into(),
            })
        }
        Some(false) => {}
    }
    if host_busy {
        return Err(Refusal::Known {
            code: "host_app_busy",
            message: "this bridge's own com host is busy with another verb".into(),
        });
    }
    // P5 — responding.
    match responding {
        None => {
            return Err(Refusal::Unknown {
                what: "whether the SOLIDWORKS window is responding",
                detail: "IsHungAppWindow could not be read for the main frame.".into(),
            })
        }
        Some(false) => {
            return Err(Refusal::Known {
                code: "host_app_hung",
                message: "the SOLIDWORKS window is not responding — a quit request cannot be \
                          delivered to a message loop that is not running"
                    .into(),
            })
        }
        Some(true) => {}
    }
    // A launch we started is still in flight: quitting into it would race our
    // own supervisor and leave a half-started session nobody owns.
    if launch_in_flight {
        return Err(Refusal::Known {
            code: "launch_in_flight",
            message: "this bridge is still launching SOLIDWORKS — poll swx_status until the \
                      launch finishes before asking it to quit"
                .into(),
        });
    }
    Ok(())
}

/// The hint when a DIALOG stopped the ladder. Different from a decline in the
/// way that matters: the app is not refusing, it is ASKING — and the answer is
/// the user's to give.
pub fn blocked_hint(d: &BlockingDialog) -> String {
    let common = "This bridge never answers a dialog. Closing a stuck SOLIDWORKS modal through \
                  the window manager took the whole application down on 2026-09-02, and the \
                  ladder stops at the first new window for exactly that reason — nothing was \
                  clicked, nothing was Escaped, and no further rung was tried. swx_state takes a \
                  BACKGROUND screenshot (it steals no focus) so you can show the user what is on \
                  screen. ANSWERING IT IS THEIR DECISION.";
    let title = &d.title;
    match d.kind {
        "save_changes" => format!(
            "SOLIDWORKS IS ASKING WHETHER TO SAVE: {title:?}. At a VERIFIED zero open documents \
             this dialog should be impossible, so its appearance means the document count this \
             bridge read was WRONG — a bug worth reporting, not a dialog to dismiss. The buttons \
             offer to write the user's work to This PC or to 3DEXPERIENCE, or to discard it. {common}"
        ),
        "license" => format!(
            "SOLIDWORKS IS ASKING ABOUT THE 3DEXPERIENCE SESSION OR LICENCE: {title:?}. A \
             sign-out affects the user's whole platform session, not just this application, and \
             this pass found no vendor documentation of a safe default — so there is no safe \
             choice for the bridge to make on their behalf. {common}"
        ),
        "document_recovery" => format!(
            "SOLIDWORKS IS SHOWING DOCUMENT RECOVERY: {title:?}. The last session ended badly and \
             there is unrecovered work behind that dialog. {common} swx_check_recovery describes it."
        ),
        _ => format!(
            "SOLIDWORKS RAISED A WINDOW THIS BRIDGE DOES NOT RECOGNISE: {title:?}. An \
             unanswerable question is not permission. {common}"
        ),
    }
}

/// The hint on a declined quit. It names what a HUMAN should click, and it must
/// never mention `swx_kill`.
pub fn declined_hint(rungs_tried: usize, max_rung: &str) -> String {
    format!(
        "SOLIDWORKS DID NOT QUIT, and nothing was forced. {rungs_tried} rung(s) up to '{max_rung}' \
         were tried and each one's measurement is in `rungsTried` — read `result` and \
         `processGone` per rung rather than the summary. WHAT A HUMAN SHOULD DO: ask the user to \
         click File ▸ Exit (or the window's X) in SOLIDWORKS themselves; that gesture works when \
         these do not, and it is theirs to make. If the ladder was pinned (`maxRung` defaults to \
         \"{}\" until the live quit gate has run), a higher rung may work — see \
         docs/QUIT-INVESTIGATION.md §5.3. Do NOT reach for a force-kill: on a Connected edition a \
         killed session leaves platform objects locked to the user, un-lockable except by hand, \
         and orphans the per-session children.",
        DEFAULT_MAX_RUNG
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    fn id() -> Identity {
        Identity { pid: 107688, created_at: 1_788_899_741, main_hwnd: Some(6_622_700) }
    }

    fn proc(pid: u32, image: &str, created: Option<u64>) -> ProcInfo {
        ProcInfo { pid, image: image.into(), created_at: created, ppid: None }
    }

    fn win(hwnd: i64, class: &str, title: &str) -> WindowInfo {
        WindowInfo { hwnd, class_name: class.into(), title: title.into(), visible: true, ..Default::default() }
    }

    /// WHICH WINDOWS BLOCK — a cases table of measured evidence → verdict (0.9.4).
    /// Each row says which of its fields were MEASURED on the live machine.
    #[test]
    fn windows_are_judged_by_measured_modality() {
        use crate::launchos::DialogContents;
        let contents = |controls: &[(&str, bool)], text: &[&str], buttons: &[&str]| DialogContents {
            text: text.iter().map(|t| t.to_string()).collect(),
            buttons: buttons.iter().map(|t| t.to_string()).collect(),
            controls: controls.iter().map(|(c, v)| (c.to_string(), *v)).collect(),
            ..Default::default()
        };
        let window = |title: &str, owner_enabled: Option<bool>, exstyle: Option<u32>| WindowInfo {
            hwnd: 331370,
            class_name: "#32770".into(),
            title: title.into(),
            visible: true,
            owner: Some(4242),
            owner_enabled,
            style: Some(0x9400_0044),
            exstyle,
            rect: Some([533, 45, 834, 112]),
        };
        // THE CANONICAL NOTIFICATION — every field MEASURED 2026-10-01 on Drew's
        // machine: hwnd 331370, owner = the main frame, owner ENABLED for all 20
        // reads over 95 s, exstyle 0x90080 (TOOLWINDOW | LAYERED | CONTROLPARENT),
        // 301x67 px at the top of the frame, ALL children Static, the visible ones
        // "Title"/"Description" and the hidden ones "Button1"/"Button2"/
        // "Hyperlink Text 1"/"Hyperlink Text 2" — template placeholders: the
        // window showed "Connected to OI000616940 — SOLIDWORKS is connected to
        // your 3DEXPERIENCE platform" (known as 3dexperience_connected; that name
        // is documentation, not a rule).
        let connected = contents(
            &[("Static", true), ("Static", true), ("Static", false), ("Static", false), ("Static", false), ("Static", false)],
            &["Title", "Description", "Button1", "Button2", "Hyperlink Text 1", "Hyperlink Text 2"],
            &[],
        );
        // THE LICENCE DIALOG — MEASURED 2026-09-17: its title "License Manager",
        // its text and its one button (OK). NOT measured: whether its owner frame
        // was disabled. It blocks BY KIND, whatever the owner says.
        let license = contents(&[("Static", true), ("Button", true)], &["Failed to check 1 license(s): …"], &["OK"]);
        let with_button = contents(&[("Static", true), ("Button", true)], &["Continue?"], &["OK"]);
        let modal_statics = contents(&[("Static", true)], &["Working…"], &[]);
        let custom_save = contents(
            &[("Static", true), ("Static", true), ("Static", true), ("Static", true)],
            &["Do you want to save changes to Part1.SLDPRT?", "Save", "Don't Save", "Cancel"],
            &[],
        );
        let two_choices = contents(&[("Static", true), ("Static", false), ("Static", false)], &["Proceed?", "&Yes", "No"], &[]);
        let recovered_text = contents(&[("Static", true)], &["SOLIDWORKS recovered 2 documents"], &[]);
        let one_close = contents(&[("Static", true), ("Static", true)], &["Connected", "Close"], &[]);
        type Row<'a> = (&'a str, &'a str, Option<bool>, Option<u32>, Option<&'a DialogContents>, bool, &'a str);
        let rows: Vec<Row> = vec![
            ("the 3DEXPERIENCE connected notice (measured)", "", Some(true), Some(0x90080), Some(&connected), false, "notification"),
            ("the same window, owner frame DISABLED", "", Some(false), Some(0x90080), Some(&connected), true, "unknown"),
            ("the same window, owner enablement UNREAD", "", None, Some(0x90080), Some(&connected), true, "unknown"),
            ("the same window, controls UNREADABLE", "", Some(true), Some(0x90080), None, true, "unknown"),
            ("the same window, extended style UNREAD", "", Some(true), None, Some(&connected), true, "unknown"),
            ("the same window, NOT a tool window", "", Some(true), Some(0x10000), Some(&connected), true, "unknown"),
            ("an unknown window with a Button, owner enabled", "", Some(true), Some(0x90080),
             Some(&with_button), true, "unknown"),
            ("an unknown MODAL: owner disabled, statics only", "", Some(false), Some(0x10000),
             Some(&modal_statics), true, "unknown"),
            ("the licence dialog (blocking by kind; owner not measured)", "License Manager", None, None, Some(&license), true, "license"),
            ("the licence dialog even with an enabled owner", "License Manager", Some(true), Some(0x90080), Some(&license), true, "license"),
            ("a save prompt, by kind", "Save Modified Documents", Some(true), Some(0x90080), Some(&connected), true, "save_changes"),
            // 0.9.5, adversary pass 43 — the window's WORDS (not measured live:
            // constructed from the measured notice's evidence).
            ("a custom-drawn save prompt: notice evidence, the question in its text", "SOLIDWORKS", Some(true), Some(0x90080),
             Some(&custom_save), true, "save_changes"),
            ("a custom-drawn choice: notice evidence, two button-like Static captions", "", Some(true), Some(0x90080),
             Some(&two_choices), true, "unknown"),
            ("ONE button-like caption is not a choice", "", Some(true), Some(0x90080), Some(&one_close), false, "notification"),
            ("the measured notice WITH a licence-type title: evidence beats a title-only guess", "3DEXPERIENCE Platform",
             Some(true), Some(0x90080), Some(&connected), false, "notification"),
            ("...and the same title on a modal still blocks, as license", "3DEXPERIENCE Platform", Some(false), Some(0x90080),
             Some(&connected), true, "license"),
            ("a recovery prompt in its TEXT, notice evidence", "", Some(true), Some(0x90080),
             Some(&recovered_text), true, "document_recovery"),
            ("a recovery title is never overridden by evidence", "Document Recovery", Some(true), Some(0x90080),
             Some(&connected), true, "document_recovery"),
            ("an import-diagnostics title is never overridden by evidence", "Import Diagnostics", Some(true), Some(0x90080),
             Some(&connected), true, "import_diagnostics"),
        ];
        for (label, title, owner_enabled, exstyle, c, want_blocking, want_kind) in rows {
            let (blocking, kind, why) = judge_window(&window(title, owner_enabled, exstyle), c);
            assert_eq!((blocking, kind), (want_blocking, want_kind), "{label}: {why}");
        }
        assert!(text_is_template_placeholder(&connected), "the template's captions are not the window's words");
        assert!(!text_is_template_placeholder(&license));
        // The measured placeholders are NOT button-like: the notice stays a notice.
        assert!(button_like_captions(&connected).is_empty(), "template placeholders are not choices");
        assert_eq!(button_like_captions(&custom_save), vec!["Save", "Don't Save", "Cancel"]);
    }

    /// The signature that matters: everything measured, nothing waived.
    fn ok_preconditions(force: bool) -> Result<(), Refusal> {
        check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()),
            false, force,
        )
    }

    #[test]
    fn a_session_with_open_documents_is_never_quit_even_with_force() {
        // THE REGRESSION TEST FOR THE LIVE POSTURE. On 2026-09-08 five of Drew's
        // clean documents were open with unsavedDocuments:0, and v1 gated on the
        // UNSAVED count — so a bare swx_stop would have tried to quit them.
        for force in [false, true] {
            let r = check_preconditions(
Some(5), Some(5), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()),
                false, force,
            );
            assert_eq!(
                r.as_ref().err().map(|e| e.code()),
                Some("open_documents_present"),
                "5 open documents must refuse (force={force}): {r:?}"
            );
        }
    }

    #[test]
    fn force_waives_the_unsaved_question_and_nothing_else() {
        // P2 only.
        assert!(check_preconditions(
Some(0), Some(0), false, Some(3), None, true, Some(false), false, Some(true), Some(&id()),
            false, true
        )
        .is_ok());
        assert_eq!(
            check_preconditions(
Some(0), Some(0), false, Some(3), None, true, Some(false), false, Some(true), Some(&id()),
                false, false
            )
            .err()
            .map(|e| e.code()),
            Some("unsaved_documents")
        );
        // ...and force waives NONE of the others.
        let dialog = BlockingDialog { hwnd: 9, title: "Save changes?".into(), kind: "save_changes", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };
        let cases: Vec<(&str, Result<(), Refusal>)> = vec![
            ("open docs", check_preconditions(
Some(2), Some(2), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, true)),
            ("truncated", check_preconditions(
None, None, true, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, true)),
            ("dialog", check_preconditions(
Some(0), Some(0), false, Some(0), Some(&dialog), true, Some(false), false, Some(true), Some(&id()), false, true)),
            ("census unreadable", check_preconditions(
Some(0), Some(0), false, Some(0), None, false, Some(false), false, Some(true), Some(&id()), false, true)),
            ("busy", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(true), false, Some(true), Some(&id()), false, true)),
            ("host busy", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), true, Some(true), Some(&id()), false, true)),
            ("hung", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(false), Some(&id()), false, true)),
            ("no identity", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), None, false, true)),
            ("launch in flight", check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), Some(&id()), true, true)),
        ];
        for (what, r) in cases {
            assert!(r.is_err(), "force must not waive {what}");
        }
    }

    #[test]
    fn every_unknown_refuses_with_one_branchable_code() {
        // A caller's move is the same for all of them, and it is never "proceed".
        let unknowns: Vec<Result<(), Refusal>> = vec![
            check_preconditions(
None, None, false, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, false),
            check_preconditions(
None, None, true, Some(0), None, true, Some(false), false, Some(true), Some(&id()), false, false),
            check_preconditions(
Some(0), Some(0), false, None, None, true, Some(false), false, Some(true), Some(&id()), false, false),
            check_preconditions(
Some(0), Some(0), false, Some(0), None, false, Some(false), false, Some(true), Some(&id()), false, false),
            check_preconditions(
Some(0), Some(0), false, Some(0), None, true, None, false, Some(true), Some(&id()), false, false),
            check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, None, Some(&id()), false, false),
            check_preconditions(
Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true), None, false, false),
        ];
        for r in unknowns {
            assert_eq!(r.err().map(|e| e.code()), Some("stop_preconditions_unknown"));
        }
        assert!(ok_preconditions(false).is_ok(), "a fully-measured clean session may proceed");
    }

    #[test]
    fn a_save_prompt_gets_its_own_code_because_it_should_be_impossible() {
        let save = BlockingDialog { hwnd: 9, title: "Save changes to Part1?".into(), kind: "save_changes", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };
        assert_eq!(save.error_code(), "stop_blocked_by_save_prompt");
        let other = BlockingDialog { hwnd: 9, title: "3DEXPERIENCE sign in".into(), kind: "license", contents: None, window: None, blocking: true, why: String::new(), owner_settle: None };
        assert_eq!(other.error_code(), "stop_blocked_by_dialog");
        // ...and each stops the ladder from the precondition sweep.
        for d in [&save, &other] {
            let r = check_preconditions(
Some(0), Some(0), false, Some(0), Some(d), true, Some(false), false, Some(true), Some(&id()),
                false, true,
            );
            assert_eq!(r.err().map(|e| e.code()), Some(d.error_code()));
        }
    }

    #[test]
    fn the_dialog_census_finds_a_modal_and_ignores_the_frame() {
        let windows = vec![
            win(6_622_700, "Afx:00007FF6", "SOLIDWORKS Design"),
            win(9001, "#32770", "Save Modified Documents"),
        ];
        let d = first_blocking_dialog(&windows).expect("the modal is found");
        assert_eq!(d.hwnd, 9001);
        assert_eq!(d.kind, "save_changes");
        // The main frame alone is NOT a dialog.
        assert!(first_blocking_dialog(&windows[..1]).is_none());
        // An invisible dialog is not on screen.
        let mut hidden = win(9002, "#32770", "Save changes?");
        hidden.visible = false;
        assert!(first_blocking_dialog(&[hidden]).is_none());
    }

    #[test]
    fn process_gone_needs_the_creation_time_and_an_unreadable_census_is_unknown() {
        let alive = vec![proc(107688, "SLDWORKS.exe", Some(1_788_899_741))];
        assert_eq!(process_gone(Ok(&alive), &id()), Some(false));
        // Same pid, DIFFERENT creation time: our session is gone and something
        // else has the number.
        let reused = vec![proc(107688, "SLDWORKS.exe", Some(1_788_999_999))];
        assert_eq!(process_gone(Ok(&reused), &id()), Some(true));
        let empty: Vec<ProcInfo> = Vec::new();
        assert_eq!(process_gone(Ok(&empty), &id()), Some(true));
        // UNREADABLE: a rung that cannot be measured is a rung that failed.
        let err = "census failed".to_string();
        assert_eq!(process_gone(Err(&err), &id()), None);
    }

    #[test]
    fn the_expected_survivors_are_not_treated_as_a_failure() {
        // The pid is gone, the children are reaped, and the platform daemons are
        // still alive — which is a CLEAN quit, by design (§2.6).
        let census = vec![
            proc(6076, "3DEXPERIENCELauncher.exe", Some(1)),
            proc(90552, "3DEXPERIENCELauncherSysTray.exe", Some(1)),
            proc(700, "EdmServerV6.exe", Some(1)),
            proc(701, "ENOPLMCSAClient.exe", Some(1)),
            proc(9, "ViewServerV6.exe", Some(1)),
        ];
        assert_eq!(process_gone(Ok(&census), &id()), Some(true));
        let (children, survivors, unjudged) = reap_report(&census);
        assert!(children.is_empty(), "no per-session child survived: {children:?}");
        assert_eq!(survivors.len(), 4, "the daemons are alive and that is consent: {survivors:?}");
        assert_eq!(unjudged, vec!["ViewServerV6.exe"], "recorded, not judged");
    }

    #[test]
    fn the_ladder_order_is_fixed_and_pinned_at_a_by_default() {
        assert_eq!(RUNGS, &["a", "b", "c", "d", "e", "f"]);
        assert_eq!(DEFAULT_MAX_RUNG, "a", "shipped pinned until the live gate has run");
        assert_eq!(rungs_up_to("a"), vec!["a"]);
        assert_eq!(rungs_up_to("c"), vec!["a", "b", "c"]);
        assert_eq!(rungs_up_to("f").len(), 6);
        assert!(rungs_up_to("g").is_empty(), "an unknown max yields nothing to try");
        assert!(!is_rung("g"));
        // There is no rung (g), and no rung mentions a kill.
        for r in RUNGS {
            let a = rung_action(r).to_lowercase();
            for forbidden in ["kill", "terminate"] {
                assert!(!a.contains(forbidden), "rung {r} action mentions '{forbidden}': {a}");
            }
            // `force` may appear ONLY as the thing being omitted.
            if a.contains("force") {
                assert!(
                    a.contains("omitted"),
                    "rung {r} mentions force without saying it is omitted: {a}"
                );
            }
            assert!(rung_budget_secs(r) >= 30);
        }
        assert!(rung_action("f").contains("OMITTED"), "the Restart Manager rung says what it omits");
    }

    #[test]
    fn a_result_the_host_does_not_explain_is_unmeasured_not_declined() {
        assert_eq!(result_from_host("exited"), RungResult::Quit);
        assert_eq!(result_from_host("declined"), RungResult::Declined);
        assert_eq!(result_from_host("uipiBlocked"), RungResult::UipiBlocked);
        assert_eq!(result_from_host("dialogAppeared"), RungResult::DialogAppeared);
        assert_eq!(result_from_host("skipped"), RungResult::Skipped);
        assert_eq!(result_from_host("something new"), RungResult::Unmeasured);
        // Which of them stop the ladder.
        assert!(RungResult::Quit.stops_ladder());
        assert!(RungResult::DialogAppeared.stops_ladder());
        assert!(RungResult::UipiBlocked.stops_ladder());
        assert!(!RungResult::Declined.stops_ladder());
        assert!(!RungResult::Skipped.stops_ladder());
        assert!(!RungResult::Unmeasured.stops_ladder());
    }

    #[test]
    fn a_count_that_disagrees_with_the_enumeration_is_unknown_not_zero() {
        // GetDocumentCount and GetDocuments are two different COM reads. The
        // Python gate has always cross-checked them; the verb did not, so a
        // count of 0 beside a list of 1 was "nothing open, go ahead".
        let r = check_preconditions(
            Some(0), Some(1), false, Some(0), None, true, Some(false), false, Some(true),
            Some(&id()), false, true,
        );
        assert_eq!(r.as_ref().err().map(|e| e.code()), Some("stop_preconditions_unknown"), "{r:?}");
        assert!(r.unwrap_err().message().contains("DISAGREE"));
        // Agreement, and an absent enumeration, both proceed — above the cap the
        // truncated refusal has already fired.
        assert!(check_preconditions(
            Some(0), Some(0), false, Some(0), None, true, Some(false), false, Some(true),
            Some(&id()), false, true
        )
        .is_ok());
        assert!(check_preconditions(
            Some(0), None, false, Some(0), None, true, Some(false), false, Some(true),
            Some(&id()), false, true
        )
        .is_ok());
    }

    #[test]
    fn the_dialog_census_covers_the_cef_children_and_only_ours() {
        // A 3DEXPERIENCE sign-in window is a web view: its hwnd belongs to
        // swCefSubProc.exe, a CHILD of sldworks.exe. Censusing the SOLIDWORKS pid
        // alone proved "no dialog" while the platform held a modal.
        let ours = 107688u32;
        let census = vec![
            proc(ours, "SLDWORKS.exe", Some(1000)),
            ProcInfo { pid: 501, image: "swCefSubProc.exe".into(), created_at: Some(1001), ppid: Some(ours) },
            ProcInfo { pid: 502, image: "swCefSubProc.exe".into(), created_at: Some(1002), ppid: Some(ours) },
            // ANOTHER session's web view — never ours to judge or to send to.
            ProcInfo { pid: 777, image: "swCefSubProc.exe".into(), created_at: Some(1003), ppid: Some(999) },
            proc(600, "sldProcMon.exe", Some(1004)),
        ];
        let pids = dialog_census_pids(Ok(&census), &id()).expect("a readable census");
        assert_eq!(pids, vec![ours, 501, 502], "sldworks + its OWN cef children: {pids:?}");
        // An unreadable census is UNKNOWN, which refuses — never an empty list,
        // which would read as "no windows to worry about".
        let e = "census unreadable".to_string();
        assert!(dialog_census_pids(Err(&e), &id()).is_none());
    }

    /// A WRONG ATTRIBUTION RECORD MUST NOT BE ABLE TO END A DIFFERENT SESSION.
    /// The ladder builds its Identity from the COM pid; the launch record is a
    /// claim about one process. If they name different processes, the claim does
    /// not cover the act.
    #[test]
    fn the_launch_claim_must_be_about_the_session_com_answers_for() {
        let me = id();
        // No claim at all: nothing to compare, and the other preconditions stand.
        assert!(identity_matches_attribution(None, Some(&me)).is_ok());
        // The claim IS about this session.
        assert!(identity_matches_attribution(Some((me.pid, me.created_at)), Some(&me)).is_ok());
        // A different pid, or the same pid with a different creation time: refuse.
        for claim in [(me.pid + 1, me.created_at), (me.pid, me.created_at + 1)] {
            let r = identity_matches_attribution(Some(claim), Some(&me));
            assert_eq!(
                r.as_ref().err().map(|e| e.code()),
                Some("identity_mismatch"),
                "a claim about {claim:?} must not authorise acting on {me:?}"
            );
        }
    }

    #[test]
    fn a_recycled_window_handle_is_never_sent_to() {
        let me = id();
        // The same window: pid AND creation time agree.
        assert!(hwnd_still_owned(Some(me.pid), Some(me.created_at), &me));
        // The pid was REUSED: same number, different process.
        assert!(!hwnd_still_owned(Some(me.pid), Some(me.created_at + 5), &me));
        // Somebody else's window on our handle.
        assert!(!hwnd_still_owned(Some(me.pid + 1), Some(me.created_at), &me));
        // Either fact unreadable is a NO. Rungs (c)/(d) skip rather than send.
        assert!(!hwnd_still_owned(None, Some(me.created_at), &me));
        assert!(!hwnd_still_owned(Some(me.pid), None, &me));
        assert!(!hwnd_still_owned(None, None, &me));
    }

    #[test]
    fn the_declined_hint_never_recommends_a_kill() {
        let h = declined_hint(3, "c").to_lowercase();
        assert!(h.contains("file ▸ exit") || h.contains("file "), "it names the human gesture: {h}");
        assert!(!h.contains("swx_kill"), "never recommends the kill verb: {h}");
        assert!(h.contains("do not reach for a force-kill"));
    }
}

/// The shipped exit macro, if it is there.
///
/// Rung (e) runs `RunMacro2` on a two-line VBA project
/// (`Sub main() : Application.SldWorks.ExitApp : End Sub`) — the only rung whose
/// request comes from INSIDE the process being asked to quit, where no external
/// client's reference count applies. It is deliberately not generated at
/// runtime: it is a reviewed file, matched by content hash before use.
///
/// A `.swp` is a binary VBA project that cannot be authored without SOLIDWORKS,
/// so this repository does not ship one yet and the rung reports `skipped` until
/// it does. That is the honest state: a rung that cannot run says so, rather
/// than being quietly absent from the ladder.
pub const EXIT_MACRO_NAME: &str = "swx-exit.swp";
/// SHA-256 of the reviewed macro. `None` until the file is generated and
/// reviewed on a machine with SOLIDWORKS; the rung refuses to run an unhashed
/// or mismatched file.
pub const EXIT_MACRO_SHA256: Option<&str> = None;

pub fn exit_macro_path() -> Option<String> {
    let exe = std::env::current_exe().ok()?;
    let p = exe.parent()?.join("resources").join("macros").join(EXIT_MACRO_NAME);
    if !p.is_file() {
        return None;
    }
    // A hash we have not recorded yet cannot be matched, so the rung stays
    // unavailable rather than running an unreviewed macro in somebody's session.
    EXIT_MACRO_SHA256?;
    Some(p.to_string_lossy().to_string())
}

#[cfg(windows)]
pub mod win {
    //! The window-message and Restart-Manager rungs.
    //!
    //! These live in the com host rather than being delegated to ab, for one
    //! reason: ab's close is a UIA `WindowPattern.Close`, which is the mechanism
    //! that took the whole application down on 2026-09-02, and it is unavailable
    //! whenever ab cannot be reached (`abAvailable:false` on the dev bridge).
    //! `SendMessageTimeout` / `PostMessage` are twenty lines here, synchronous,
    //! timeout-bounded and measurable.
    use windows::Win32::Foundation::{GetLastError, SetLastError, HWND, LPARAM, WPARAM, ERROR_ACCESS_DENIED, WIN32_ERROR};
    use windows::Win32::UI::WindowsAndMessaging::{
        GetWindowThreadProcessId, PostMessageW, SendMessageTimeoutW, SMTO_ABORTIFHUNG, SMTO_NORMAL,
        WM_CLOSE, WM_SYSCOMMAND,
    };

    /// WHOSE WINDOW IS THIS, RIGHT NOW? `None` when the handle names no window
    /// any more.
    ///
    /// Handles are recycled. The hwnd a message rung sends to was captured at
    /// precondition time, and the rung before it exists to make that window go
    /// away — so between capture and send the handle can belong to somebody
    /// else's window, and `WM_CLOSE` to somebody else's window is a close nobody
    /// asked for.
    pub fn window_pid(h: i64) -> Option<u32> {
        let mut pid: u32 = 0;
        let tid = unsafe { GetWindowThreadProcessId(hwnd(h), Some(&mut pid)) };
        if tid == 0 || pid == 0 {
            None
        } else {
            Some(pid)
        }
    }

    /// `SC_CLOSE` — the system-menu Close.
    const SC_CLOSE: usize = 0xF060;

    fn hwnd(h: i64) -> HWND {
        HWND(h as *mut std::ffi::c_void)
    }

    /// UIPI FAILURES RETURN SUCCESS. `SendMessage`/`PostMessage` to a
    /// higher-integrity window fail with `ERROR_ACCESS_DENIED` and a return value
    /// indistinguishable from success, so `SetLastError(0)` first and read
    /// `GetLastError()` after — an unchecked one reports a quit that never
    /// happened (docs/QUIT-INVESTIGATION.md §2.3).
    fn blocked_by_uipi() -> bool {
        let last = unsafe { GetLastError() };
        last == ERROR_ACCESS_DENIED
    }

    /// Rung (c): what clicking X does, and on MFC literally what File ▸ Exit
    /// does. Timeout-bounded so a modal cannot wedge the com host.
    pub fn send_wm_close(h: i64) -> (bool, bool, String) {
        unsafe { SetLastError(WIN32_ERROR(0)) };
        let mut out: usize = 0;
        let r = unsafe {
            SendMessageTimeoutW(
                hwnd(h),
                WM_CLOSE,
                WPARAM(0),
                LPARAM(0),
                SMTO_ABORTIFHUNG | SMTO_NORMAL,
                5_000,
                Some(&mut out as *mut usize),
            )
        };
        let blocked = blocked_by_uipi();
        let ok = r.0 != 0;
        (
            ok && !blocked,
            blocked,
            format!(
                "SendMessageTimeout(WM_CLOSE) -> hwnd {h}: returned {}, result {out}{}",
                r.0,
                if blocked { ", ERROR_ACCESS_DENIED (UIPI)" } else { "" }
            ),
        )
    }

    /// Rung (d): the system-menu Close. Posted, so it cannot block, and it
    /// enters through `OnSysCommand` before `DefWindowProc` turns it into a
    /// `WM_CLOSE` — an app that filters one may honour the other.
    pub fn post_sc_close(h: i64) -> (bool, bool, String) {
        unsafe { SetLastError(WIN32_ERROR(0)) };
        let posted =
            unsafe { PostMessageW(Some(hwnd(h)), WM_SYSCOMMAND, WPARAM(SC_CLOSE), LPARAM(0)) };
        let blocked = blocked_by_uipi();
        let ok = posted.is_ok();
        (
            ok && !blocked,
            blocked,
            format!(
                "PostMessage(WM_SYSCOMMAND, SC_CLOSE) -> hwnd {h}: {}{}",
                if ok { "posted" } else { "refused" },
                if blocked { ", ERROR_ACCESS_DENIED (UIPI)" } else { "" }
            ),
        )
    }

    /// Rung (f): the OS's own POLITE shutdown, and the only route that takes the
    /// pid **and its creation time** — the same identity pair the attribution law
    /// demands.
    ///
    /// **`RmForceShutdown` IS NOT REACHABLE FROM THIS CODE.** The flags argument
    /// is a literal `0`, not a parameter and not a runtime option: with the force
    /// flag omitted `RmShutdown` never terminates anything, and a refusal comes
    /// back as `ERROR_FAIL_SHUTDOWN (351)` with a readable per-process status.
    /// It sends `WM_QUERYENDSESSION(ENDSESSION_CLOSEAPP)` — which means "save
    /// automatically without prompting" — which is why this rung is LAST.
    pub fn restart_manager_shutdown(pid: u32, created_at_epoch: u64) -> (bool, String) {
        use windows::core::PWSTR;
        use windows::Win32::Foundation::{FILETIME, ERROR_SUCCESS};
        use windows::Win32::System::RestartManager::{
            RmEndSession, RmRegisterResources, RmShutdown, RmStartSession, RM_UNIQUE_PROCESS,
            CCH_RM_SESSION_KEY,
        };

        let mut session: u32 = 0;
        // CCH_RM_SESSION_KEY wide chars plus the terminator.
        let mut key = vec![0u16; CCH_RM_SESSION_KEY as usize + 1];
        let started =
            unsafe { RmStartSession(&mut session, None, PWSTR(key.as_mut_ptr())) };
        if started != ERROR_SUCCESS {
            return (false, format!("RmStartSession failed: {}", started.0));
        }
        // epoch seconds -> FILETIME (100ns ticks since 1601-01-01).
        let ticks = (created_at_epoch + 11_644_473_600) * 10_000_000;
        let ft = FILETIME {
            dwLowDateTime: (ticks & 0xFFFF_FFFF) as u32,
            dwHighDateTime: (ticks >> 32) as u32,
        };
        let target = [RM_UNIQUE_PROCESS { dwProcessId: pid, ProcessStartTime: ft }];
        let reg = unsafe { RmRegisterResources(session, None, Some(&target), None) };
        if reg != ERROR_SUCCESS {
            unsafe {
                let _ = RmEndSession(session);
            }
            return (false, format!("RmRegisterResources failed: {}", reg.0));
        }
        // ═══ FLAGS = 0. `RmForceShutdown` IS NOT WRITTEN ANYWHERE IN THIS FILE.
        // Not a parameter, not a runtime option, not a named constant that could
        // be passed by mistake — a literal zero. With the force flag omitted
        // RmShutdown NEVER terminates anything: it sends
        // WM_QUERYENDSESSION(ENDSESSION_CLOSEAPP) then WM_ENDSESSION (and a
        // WM_CLOSE to GUI apps that ignore those), and a refusal comes back as
        // ERROR_FAIL_SHUTDOWN (351) rather than a dead process.
        let shutdown = unsafe { RmShutdown(session, 0, None) };
        unsafe {
            let _ = RmEndSession(session);
        }
        let ok = shutdown == ERROR_SUCCESS;
        let note = if ok {
            format!("RmShutdown(pid {pid}, force OMITTED) returned success")
        } else {
            format!(
                "RmShutdown(pid {pid}, force OMITTED) refused: Win32 error {} \
                 (351 = ERROR_FAIL_SHUTDOWN, meaning the app declined the OS's own polite request; \
                 nothing was terminated)",
                shutdown.0
            )
        };
        (ok, note)
    }
}