"""fusion_describe — self-describe the bridge's verb surface for Adom Desktop.

AD v1.9.23+ added a Verbs tab to each bridge's detail pop-out (lists verbs,
expands each for its I/O + timeout + status schema, with an inline runner). AD
does NOT hardcode a cloud-owned bridge's verbs, so the bridge must self-describe.
AD POSTs {command:"describe", args:{}} and caches the result at ~/.adom/bridge-verbs/.

Each entry: name, summary, input, output, timeoutSeconds, statusVerb, longRunning,
example. Keep this in step with the actual handlers as verbs are added.
"""

_STATUS = "fusion_get_app_state"

# Compact source table: name -> (summary, input, output, timeout, statusVerb, longRunning, example)
_T = [
    # ── Lifecycle ──────────────────────────────────────────────────────────
    ("fusion_start", "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.",
     {}, {"success": "bool", "addinReady": "bool", "status": "str"}, 150, None, True, {}),
    ("fusion_stop", "Gracefully STOP Fusion 360: close docs cleanly + WM_CLOSE, then wait. NO force-kill - the clean opposite of fusion_start. If a window will not close it returns failure; use fusion_kill then.",
     {"skipCleanClose": "optional bool"}, {"success": "bool", "output": "str"}, 60, None, False, {}),
    ("fusion_kill", "Force-KILL Fusion 360 (taskkill /F) - the desperate path for when fusion_stop cannot close it (stuck modal / wedged process). Closes docs cleanly first when reachable.",
     {"skipCleanClose": "optional bool"}, {"success": "bool", "output": "str"}, 30, None, False, {}),
    ("fusion_close_window", "Close the active Fusion document/window.",
     {"name": "optional str (default: active)"}, {"success": "bool"}, 30, None, False, {}),
    ("fusion_get_app_state", "Current app state: running, active document, workspace, isElectronics. The poll/status verb for long-running calls.",
     {}, {"activeDocument": "str", "activeWorkspace": "str", "isElectronics": "bool"}, 20, None, False, {}),
    ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? AD DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (AD >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with AD's bridge_readiness.",
     {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}),

    ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",
     {"schPath": "required str (Windows path to .sch)", "brdPath": "optional str (defaults to sibling .brd)"},
     {"success": "bool", "imported": "list"}, 200, None, False,
     {"schPath": "C:/Users/me/adom-lib/MyBoard.sch", "brdPath": "C:/Users/me/adom-lib/MyBoard.brd"}),

    ("fusion_notify_owner", "LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-AD to every peer on the relay, so it reaches them even when this bridge runs on an unattended VM). ONLY for true human walls - password/2FA entry, UAC elevation, a physical step. Exhaust programmatic options first (UIA background clicks, seat auto-resolve, warm-SSO sign-in). After sending, WAIT and poll fusion_readiness; do not re-toast within a few minutes.",
     {"title": "optional str", "body": "required str", "level": "optional str info|warning|error"},
     {"success": "bool", "targets": "list"}, 20, None, False,
     {"title": "Fusion needs you", "body": "Please complete the Autodesk 2FA prompt on winvm - everything else is done."}),

    ("fusion_signin", "Sign Fusion in through the CORRECT browser profile - the fix for Fusion's dumb OAuth that fires at the OS-DEFAULT browser and lands in the wrong Autodesk account. Fusion opens its sign-in in whatever browser is default (e.g. a personal Chrome), but a user's Autodesk seat often lives in a different profile (work). This verb reads the FULL authorize URL (client_id/PKCE/state/request_id) that Fusion wrote into that wrong browser's HISTORY and re-opens it in the RIGHT profile IN THE BACKGROUND; the URL completes back to the running Fusion via the idmgr/callback + autodesk:// handoff, so the browser no longer has to be the OS default. Pass {profile:'chrome:you@work.com'} to fix + remember your Autodesk profile; else it probes each profile for a live Autodesk session (or asks). Staged: if Fusion hasn't opened the browser yet it returns stage:'click_signin' so you click Fusion's Sign In first. BEST: pass {auto:true} to run the WHOLE chain in ONE server-side call - it clicks Fusion's Sign In, polls the browser history on-box for the fresh authorize URL, and re-opens it in the target profile, all without extra remote round-trips, so it fits inside Fusion's ~2-min OAuth request expiry even on a slow/flaky relay (the manual staged path is the fallback). Fusion's request EXPIRES in ~2 min, so a stale captured URL returns stage:'stale' - restart Fusion (fusion_stop/start) for a fresh request and retry auto PROMPTLY. See the 'fusion-multiprofile-signin' skill.",
     {"profile": "optional str (chrome:<email>/edge:<email> - the profile your Autodesk account is in; remembered)", "auto": "optional bool (do click+capture+reopen in one on-box call - recommended)", "waitSec": "optional int (server-side poll budget for the fresh URL, default 32)"},
     {"stage": "str", "done": "bool", "signinWhere": "str", "signinProfile": "str", "signinProfileReason": "str", "signinWrongBrowser": "str", "signinAuthUrl": "str", "signinOpenedVia": "str (extension|extension_free)", "signinWall": "str|null (twofactor_email_code|credentials|protocol_handoff)", "notifyDelivered": "bool", "signinWallHint": "str|null", "narrate": "str"},
     120, "fusion_signin", True, {}),
    ("fusion_mcp_status", "Is Autodesk's Fusion MCP server up, and what does it expose? Autodesk + Anthropic ship a LOCAL MCP server at http://127.0.0.1:27182/mcp exposing Fusion's own text-to-CAD surface (read geometry, execute/update features, read Electronics design data). It binds LOOPBACK on the user's machine and was built for Claude Desktop running there, so a CLOUD AI cannot reach it at all - this bridge runs on that machine and proxies it for you. Returns enabled, serverInfo and the live tool list. OFF by default: if it reports enabled:false, call fusion_mcp_enable and the bridge turns it on for the user.",
     {}, {"enabled": "bool", "serverInfo": "obj", "toolCount": "int", "tools": "[{name,description}]"},
     45, "fusion_mcp_status", False, {}),
    ("fusion_mcp_enable", "Turn Autodesk's Fusion MCP server ON by driving Fusion's Preferences dialog for the user. Autodesk exposes NO API for this toggle, so the UI is the only route: this opens Preferences (Commands.Start PreferencesCommand), expands General, selects API, ticks 'Fusion MCP Server (runs locally on this device)', then clicks Apply and OK. Needs Fusion running; briefly uses the foreground and announces why with an on-screen caption. NEVER ask the user to do this by hand first - do it for them, then tell them it is handled. Note the setting is DISCARDED if Fusion is killed or restarted before Apply is clicked.",
     {}, {"enabled": "bool", "alreadyOn": "bool", "steps": "list", "serverInfo": "obj"},
     180, "fusion_mcp_status", True, {}),
    ("fusion_mcp_tools", "Full tool list from Autodesk's Fusion MCP server INCLUDING input schemas, so you can construct a valid fusion_mcp_call. These are Autodesk's tools, not this bridge's.",
     {}, {"count": "int", "tools": "list"}, 45, "fusion_mcp_status", False, {}),
    ("fusion_mcp_call", "Call any tool on Autodesk's Fusion MCP server (fusion_mcp_read, fusion_mcp_update, fusion_mcp_execute, fusion_mcp_electronics_read). The bridge handles the MCP streamable-HTTP handshake for you (initialize -> MCP-Session-Id -> notifications/initialized) and re-establishes a dropped session automatically. MCP acts on the ACTIVE Fusion document, so open one first. PREFER a native fusion_* verb when one exists - they are tested here and return richer hints - and reach for MCP for Autodesk's text-to-CAD surface.",
     {"tool": "required str (from fusion_mcp_tools)", "arguments": "optional obj (per that tool's inputSchema)", "timeout": "optional int seconds, default 120"},
     {"result": "obj", "tool": "str"}, 150, "fusion_mcp_status", True, {"tool": "fusion_mcp_read", "arguments": {"queryType": "summary"}}),
    ("fusion_mcp_resources", "List Autodesk MCP resources, or read one by uri. These are mostly the Electronics entity schemas (resource://mcp.electronics_schema_*) that describe what fusion_mcp_electronics_read can return.",
     {"uri": "optional str (omit to list)"}, {"count": "int", "resources": "[str]", "result": "obj"},
     60, "fusion_mcp_status", False, {}),
    ("fusion_prefs_open", "Open Fusion's Preferences dialog and optionally jump to a section, then hand back a SCREENSHOT + shotId you can click in. This is how you reach ANY Fusion preference: the Python API exposes only a thin slice (theme/orbit/units, debuggingPort, developer tools) and everything else, including the MCP server toggle, is UI-only. Sections: general, api, design, manufacture, electronics, render, drawing, material, graphics, network, preview features. Child sections under General are lazily rendered by Qt and never appear in the UIA tree, which is why this returns an image to click rather than control names. ALWAYS finish with fusion_prefs_close {save:true} - nothing is saved otherwise, and killing/restarting Fusion first discards the change.",
     {"section": "optional str (see knownSections in the response)"},
     {"hwnd": "int", "shotId": "str", "screenshot": "str", "knownSections": "[str]", "steps": "list"},
     120, "fusion_get_preferences", True, {"section": "api"}),
    ("fusion_prefs_close", "Apply+OK (save:true, the default) or Cancel Fusion's Preferences dialog. Settings changed via fusion_prefs_open are NOT persisted until this is called with save:true.",
     {"save": "optional bool, default true"}, {"closed": "bool", "saved": "bool"},
     60, "fusion_get_preferences", False, {"save": True}),
    ("fusion_set_auto_update", "Turn AUTOMATIC Fusion updating on/off for this user. ON BY DEFAULT, and deliberately INVISIBLE: Fusion ships updates constantly and nags with an 'Update Now / Update Later' panel plus a '14 days until Fusion must update' countdown. Every fusion_readiness call checks for that offer and, if present, the bridge clicks it IN THE BACKGROUND (UIA, no foreground, no caption) so the download starts and the newer build comes up on the next launch. The user should never have to click it. Call with no args to read the current setting; {enabled:false} opts out and the choice is STICKY (~/.adom/fusion-bridge/prefs.json) - once a user opts out, NEVER click their update prompts again. Tell the user this is handled for them rather than asking them to update.",
     {"enabled": "optional bool (omit to read current value)"},
     {"autoUpdateFusion": "bool", "narrate": "str"},
     30, "fusion_readiness", True, {"enabled": True}),
    ("fusion_signin_2fa", "Submit Autodesk's emailed 6-DIGIT verification code to finish a Fusion sign-in, typed IN THE BACKGROUND (UIA SetValue + Invoke - never steals the user's foreground or moves their cursor). Exists so you can finish a login END TO END for the user instead of parking them on a 2FA screen: if `adom-google` is available in your container, read the code out of their Gmail (`adom-google gmail search 'from:autodesk verification code' --limit 1`), pull the 6 digits, and call this. If adom-google is NOT installed, tell the user that installing it would let you automate logins like this, then let them type it. NEVER ask the user for a code you could read yourself. The bridge ALSO toasts the user's desktop whenever it hits a human wall, so you do not need to ask it to.",
     {"code": "required str (the 6 digits; non-digits are stripped)", "hwnd": "optional int (the 2FA window; auto-detected)"},
     {"submitted": "bool", "steps": "list", "narrate": "str"},
     90, "fusion_readiness", True, {"code": "123456"}),
    ("fusion_demo", "FIRST-TIME-USER DEMO in one verb - the thing to run when someone new wants to SEE what this bridge does (HD's installer calls this). Takes them from nothing to a real board: launches Fusion, FINISHES the Autodesk sign-in - it OPENS the sign-in in the user's NATIVE browser IN THE BACKGROUND (never interrupting their work) and returns signinWhere/signinProfile/signinSession so you can tell them exactly where it is waiting, then offer three paths: they finish it, you foreground it, or (with their OK) you drive it. Native browser via ABE/nbrowser_* because that profile is already signed into Autodesk - never pup, which is anonymous, sets up APS cloud search (or explains it if declined) and runs a LIVE sample search so they see 2-second results, then opens an electronics PROJECT and walks SCHEMATIC -> 2D BOARD -> 3D BOARD, screenshotting each view to show them. STAGED + RESUMABLE: every call advances as far as it safely can and returns stage/done/narrate/screenshots plus a _hint with the exact next action; keep calling it until done:true. NEVER leave a first-time user parked on the sign-in screen - that is the failure this verb exists to prevent. See the 'fusion-demo' skill.",
     {"query": "optional str (design to demo; else picked from APS search)", "stage": "optional str ('skip_aps' to proceed without cloud search)"},
     {"stage": "str", "done": "bool", "narrate": "str", "screenshots": "[{label,path}]", "steps": "[str]", "apsSampleSearch": "[{name,project}]"},
     240, "fusion_demo", True, {}),

    # ── Cloud search & files (APS) ─────────────────────────────────────────
    ("fusion_aps_status", "APS state: configured, signed in, token live, never-charge meter.",
     {}, {"configured": "bool", "signedIn": "bool", "neverCharge": "bool"}, 20, None, False, {}),
    ("fusion_aps_set_client_id", "Set the org's APS (PKCE) client id used for cloud search.",
     {"clientId": "required str"}, {"success": "bool"}, 20, None, False, {"clientId": "D5nn...EPAq"}),
    ("fusion_aps_signin", "Open the Autodesk sign-in in the REMEMBERED browser+profile (not the OS default); a background listener captures the token. Pass {exe,profileDir,name} to use+remember a specific browser, or {allowDefaultBrowser:true} to fall back to the OS default. If nothing is remembered, returns needsExtensionOpen so the caller opens authUrl in the user's real browser via nbrowser_open_window.",
     {"exe": "optional str (browser binary to launch+remember)", "profileDir": "optional str (e.g. 'Default','Profile 2')", "name": "optional str", "allowDefaultBrowser": "optional bool"},
     {"success": "bool", "authUrl": "str", "openedVia": "str", "needsExtensionOpen": "bool"}, 30, "fusion_aps_status", False, {}),
    ("fusion_aps_set_browser", "Remember the browser+profile that authed to Autodesk so every later sign-in reuses it (power users have many browsers/profiles; the OS default is usually the wrong one). Call after a driven sign-in succeeds.",
     {"name": "optional str", "exe": "str (browser binary) OR how:'extension'", "profileDir": "optional str", "how": "optional 'extension'|'launch'"},
     {"success": "bool", "browser": "{name,exe,profileDir}"}, 10, None, False, {"name": "Edge", "exe": "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe", "profileDir": "Default"}),
    ("fusion_aps_get_browser", "Show the remembered Autodesk sign-in browser (None until set).",
     {}, {"success": "bool", "browser": "{name,exe,profileDir}|null"}, 10, None, False, {}),
    ("fusion_aps_forget_browser", "Forget the remembered Autodesk sign-in browser (re-detect next time).",
     {}, {"success": "bool", "cleared": "bool"}, 10, None, False, {}),
    ("fusion_aps_search", "Server-indexed search across the whole team hub (seconds, not a 30-min folder walk).",
     {"query": "required str", "limit": "optional int (default 50)", "hubId": "optional str", "projectId": "optional str"},
     {"count": "int", "results": "[{name,id,projectName,lastModified}]"}, 60, None, False, {"query": "Molecule", "limit": 10}),
    ("fusion_aps_open", "Find a cloud design by name and open it in Fusion (any folder depth). Fire-and-poll.",
     {"query": "required str", "wait": "optional bool"}, {"opening": "bool", "match": "{name}", "statusVerb": "str"}, 60, _STATUS, True, {"query": "BME690 Molecule"}),
    ("fusion_open_by_urn", "Open a cloud ELECTRONICS DESIGN by URN. PCB/ELECTRONICS NAV RULE: a board is NOT a standalone file - the schematic, 2D board and 3D board are VIEWS inside ONE electronics DESIGN. Open the DESIGN document first (fusion_open_cloud_file or fusion_open_by_urn), THEN switch to its schematic and its 2D board (fusion_show_2d_board) and 3D board (fusion_show_3d_board). A Select-Electronics-Design-File picker IS that design list of its schematic + board. An EMPTY 2D board usually means you opened a derivative or the wrong file, or it is out of sync - reopen the parent electronics DESIGN and switch to its board view; do NOT open a .brd or .sch as a standalone file.",
     {"urn": "required str"}, {"success": "bool", "output": "str"}, 240, _STATUS, True, {"urn": "urn:adsk.wipprod:fs.file:vf.xxxx?version=1"}),
    ("fusion_aps_browse", "Browse hubs/projects/folders without launching Fusion.",
     {"hubId": "optional str", "projectId": "optional str", "folderId": "optional str"}, {"items": "[{name,type,id}]"}, 60, None, False, {}),
    ("fusion_aps_recent", "Most-recently-modified designs across the team hub, newest first.",
     {"limit": "optional int (default 25)"}, {"count": "int", "results": "[...]"}, 60, None, False, {"limit": 25}),
    ("fusion_aps_file_info", "Version history + who/when/size for a file (tip + recent versions).",
     {"query": "str  OR  projectId+urn"}, {"versionCount": "int", "tip": "{...}", "versions": "[...]"}, 40, None, False, {"query": "BME690 Molecule"}),
    ("fusion_aps_versions", "Full (uncapped) version history of a file.",
     {"query": "str  OR  projectId+urn"}, {"versionCount": "int", "versions": "[...]"}, 40, None, False, {"query": "BME690 Molecule"}),
    ("fusion_aps_download", "Download a cloud file to the local machine (no Fusion needed).",
     {"query": "str OR projectId+urn", "saveDir": "optional str", "fileName": "optional str"}, {"path": "str", "bytes": "int"}, 90, None, False, {"query": "BME690 Molecule"}),
    ("fusion_aps_upload", "Upload a local file as a NEW cloud file (free Data Management; needs data:write scope). ⛔ FOLDER HYGIENE: NEVER pass a project ROOT folderId - the bridge REFUSES it (refused_root_upload). Loose files in a shared root clutter the team's cloud. Create/choose a real subfolder first (fusion_aps_create_folder under an 'Adom AI Workspace' folder), or just use fusion_make_3d_package / fusion_build_library_3d which auto-target a non-root workspace. To RELOCATE an existing file, MOVE it (preserves its urn) - do NOT delete + re-upload (mints a new urn, breaks 3D bindings). See the fusion-cloud-hygiene skill.",
     {"projectId": "required str", "folderId": "required str (NON-ROOT subfolder)", "localPath": "required str", "fileName": "optional str"},
     {"itemUrn": "str", "bytes": "int"}, 90, None, False, {"projectId": "a.YnVz...", "folderId": "urn:...", "localPath": "C:/x.f3d"}),
    ("fusion_aps_create_folder", "Create a subfolder in a project (free; needs data:create scope). Use this to make an 'Adom AI Workspace' folder + per-task subfolders so the bridge never dumps loose files in the shared project root. See fusion-cloud-hygiene.",
     {"projectId": "required str", "parentFolderId": "required str", "name": "required str"}, {"folderId": "str"}, 50, None, False, {"projectId": "a.YnVz...", "parentFolderId": "urn:...", "name": "Adom AI Workspace"}),
    ("fusion_aps_get", "Raw authenticated GET against a free APS path (dev/verify helper).",
     {"path": "required str"}, {"status": "int", "body": "obj"}, 40, None, False, {"path": "/project/v1/hubs"}),
    ("fusion_walk_cloud_tree", "⛔ DISABLED (returns an error): the in-app add-in walk took 30+ min and CRASHED Fusion (WinError 10054). It is hard-blocked in the bridge. Use fusion_aps_search / fusion_aps_open. See the 'fusion-aps-search' skill.",
     {}, {"disabled": "bool"}, 15, None, False, {}),
    ("fusion_search_cloud_files", "⛔ DISABLED (returns an error): the in-app add-in search took 30+ min and CRASHED Fusion (WinError 10054). It is hard-blocked in the bridge. Use fusion_aps_search {query} — server-indexed, seconds, no Fusion. See the 'fusion-aps-search' skill.",
     {"query": "required str"}, {"disabled": "bool"}, 15, None, False, {"query": "Molecule"}),

    # ── Electronics ────────────────────────────────────────────────────────
    ("fusion_show_schematic", "Switch the OPEN electronics design to its SCHEMATIC view (the symmetric partner of fusion_show_2d_board / fusion_show_3d_board, via Fusion's SwitchSchDocCmd). A VIEW of the project, not a standalone file. Drive it for demos with fusion_electron_zoom / _pan / _select.",
     {}, {"success": "bool", "activeWorkspace": "str", "productType": "str"}, 70, None, True, {}),
    ("fusion_show_2d_board", "Switch the OPEN electronics design to its 2D PCB Editor (board layout) view. This is a VIEW of the project, not a file to open standalone - if the board is empty, you opened a child/derivative instead of the EcadDesignProductType PROJECT; reopen the project. Drive the view with fusion_electron_run WINDOW commands.",
     {}, {"activeWorkspace": "str"}, 70, None, True, {}),
    ("fusion_show_3d_board", "Switch the OPEN electronics design to its 3D PCB view (populated board). A VIEW of the project, not a standalone file. Orbit it for demos via fusion_run_modeling_script (app.activeViewport.camera + vp.refresh()).",
     {}, {"activeWorkspace": "str", "screenshots": "[...]"}, 70, None, True, {}),
    ("fusion_open_by_urn_note", "(see fusion_open_by_urn)", {}, {}, 30, None, False, {}),
    ("fusion_board_info", "Raw board geometry XML (copper/arcs/lines) for the open board.",
     {}, {"rawXml": "str"}, 60, None, False, {}),
    ("fusion_open_lbr", "Import an EAGLE .lbr library into Fusion's Electronics LIBRARY editor (Content Manager / Electronics Library workspace), not a PCB design. PITFALL: filePath MUST be a WINDOWS-local path (C:/...). The chip-fetcher/adom-lbr pipeline runs in the cloud container, and there is NO container-to-Windows push verb, so STAGE the .lbr onto Windows first (serve it on a proxied port + Invoke-WebRequest via shell_execute), then pass the C:/ path. PITFALL: an adom-lbr .lbr is 2D ONLY - symbol + footprint + a PLACEHOLDER 3D package; the real 3D chip is NOT attached (that needs a Fusion cloud URN). To attach the 3D, see the 'fusion-libraries' skill. Pass verify:true to round-trip-confirm Fusion parsed the deviceset, then check fusion_get_app_state (activeWorkspace 'Electronics Library').",
     {"filePath": "required str (.lbr WINDOWS path C:/...; stage from container first)", "symbolName": "optional str", "verify": "optional bool"},
     {"success": "bool", "postOpenScreenshot": "{savedTo}"}, 50, None, False, {"filePath": "C:/Users/<user>/adom-lib/nRF54L15.lbr"}),
    ("fusion_attach_3d_package", "Attach a real 3D chip model to a library package, end to end: opens the .lbr (library active), runs Electron.Create3DPackage to enter the Package3DEnvironment showing the footprint, imports the STEP + auto-orients it flat on the footprint, then executes Package3DStop (FINISH). This BINDS the 3D onto the deviceset (Content Manager shows the 3D nested under the package; the Packages 'Package' column flips Placeholder->part-name). ONE desktop-side step remains: Fusion pops a modal Save dialog (an OWNED popup) - the response _hint gives the exact desktop_ui_click to confirm it. AFTER: re-grab desktop_screenshot_window and check ownedPopupCount for errors; the 3D preview LAGS a beat before showing the real chip. Both paths (filePath/modelPath) are WINDOWS paths - stage from the container first.",
     {"filePath": "required str (.lbr WINDOWS path C:/...)", "modelPath": "required str (.step WINDOWS path C:/...)", "packageName": "optional str"},
     {"success": "bool", "savePending": "bool", "data": "{steps}", "_hint": "the Save-dialog click + verify steps"}, 240, _STATUS, True, {"filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr", "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step", "packageName": "ADS8588SIPM"}),
    ("fusion_install_fusion", "Install Fusion 360 FOR the user - downloads Autodesk's official Client Downloader and streams the free trial silently, with NO shell_execute and NO AD approval gate (runs inside the trusted bridge). Elevation-aware: --globalinstall when admin, PER-USER otherwise (a non-elevated globalinstall dies silently on UAC - learned live). Returns promptly; poll fusion_readiness (reports installing:true) until installed:true, then fusion_start. Declared as detect.installVerb so AD's bridge_readiness recommends it.",
     {}, {"success": "bool", "installing": "bool", "alreadyInstalled": "bool?", "mode": "globalinstall|per-user", "statusVerb": "str"}, 90, _STATUS, True, {}),
    ("fusion_generate_package", "Generate an IPC-7351-compliant parametric 3D package via Fusion's BUILT-IN ElectronicsPackageGenerator (EPG) - ~50 package families (chip/soic/qfn/qfp/bga/sot23/dfn/melf/ecap/crystal/headers/...), each a few seconds, ZERO GUI. Optionally LASER-ETCH a marking (MPN / '103') into the body top as REAL cut geometry (survives STEP), and export STEP in the same call - the royalty-free, license-clean path to a whole component library (our own generated output, not vendor models). Proven live: 0603 + '103' etch + STEP in ~2s.",
     {"type": "required str (EPG family, e.g. chip|soic|qfn|qfp|bga|sot23|dfn2|melf|ecap|crystal|header_straight - see supportedTypes in the error for the full list)", "params": "optional dict (generator dims in MM by default, e.g. chip: {D,E,A,L,L1}; soic: {A,A1,b,D,E,E1,e,L,DPins}; omitted keys use EPG defaults)", "unitsCm": "optional bool (params already EPG-native cm)", "etch": "optional str (marking text on the chip top: MPN / '103'; multi-line with newline, e.g. MPN+variant - laid along the LONGEST face axis)", "etchStyle": "optional str raised|engraved (default raised = thin WHITE positive extrude, silkscreen-style contrast; engraved = sunken cut)", "etchDepthMm": "optional float (marking height/depth, default 0.03)", "etchHeightMm": "optional float (text height; default auto-fit: top-5%-band face, 10% margin, width-aware)", "outputStep": "optional str (WINDOWS path; exports STEP incl. the marking)"},
     {"success": "bool", "bodies": "[{name,vol}]", "etched": "str|null", "stepPath": "str|null", "manifest": "obj (settings+why+bboxes; also written as <step>.manifest.json sidecar for marking refreshes)", "manifestPath": "str|null", "epgDir": "str"}, 120, _STATUS, True, {"type": "chip", "params": {"D": 1.6, "E": 0.8, "A": 0.45, "L": 0.3, "L1": 0.3}, "etch": "103", "outputStep": "C:/tmp/ADOM-R0603-103.step"}),
    ("fusion_make_3d_package", "Create a RENDERING component 3D package (FOOTPRINT + chip, aligned) for ONE part, fully programmatically with ZERO GUI dialogs. Opens the .lbr, runs Electron.Create3DPackage to load the package's footprint into a generator doc, imports the STEP onto it, orients it flat, saveAs an .f3d (skips the FINISH Save dialog + the two unbeatable 'Fusion360' CEF modals), uploads the f3d via APS, and returns an fs.file:vf wip_urn to hand-write into the library's <packages3d>. projectId/folderId default to the MAIN-project upload folder. Pass captureLabel to also get a BEFORE (footprint) + AFTER (chip placed) screenshot. PITFALL: a RAW STEP upload binds but renders NOTHING ('Thumbnail download failed') - this verb makes an f3d, which renders. Prefer fusion_build_library_3d for many parts.",
     {"lbrPath": "required str (.lbr WINDOWS path; its FIRST package's footprint is loaded)", "modelPath": "required str (.step WINDOWS path)", "projectId": "optional str (default MAIN-project)", "folderId": "optional str (default upload folder)", "fileName": "optional str", "captureLabel": "optional str (capture before/after)", "orient": "optional bool (default true)"},
     {"success": "bool", "wip_urn": "str", "dims_mm": "[x,y,z]", "f3d": "str", "before": "str|null", "after": "str|null"}, 200, _STATUS, True, {"lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step", "captureLabel": "R_4k7"}),
    ("fusion_build_library_3d", "Build a RENDERING multi-part 3D library in ONE call - the whole programmatic pipeline. For each part it makes the footprint+chip f3d package (no GUI dialogs, optional before/after screenshots) and collects the wip_urn, then injects ALL bindings into the combined .lbr in one pass (no hand XML surgery) and opens the finished library once. This is the verb for a basic-parts sampler / any many-part library; it replaces the per-part make_3d_package loop + manual binding. projectId/folderId default to the MAIN-project upload folder. Each part is independent, so re-run just the failed parts if Fusion resets mid-run.",
     {"lbrPath": "required str (combined .lbr WINDOWS path to bind + open)", "parts": "required [{package, lbrPath (per-part footprint .lbr), modelPath (.step)}]", "outLbrPath": "optional str (default: overwrite lbrPath)", "capture": "optional bool (default true)", "projectId": "optional str", "folderId": "optional str", "openWhenDone": "optional bool (default true)"},
     {"success": "bool", "boundLbr": "str", "partsBound": "int", "partsTotal": "int", "parts": "[{package,success,wip_urn,dims_mm,error}]", "screenshots": "[{package,stage,path}]", "opened": "bool"}, 1800, _STATUS, True, {"lbrPath": "C:/tmp/newlib/AdomBasicParts2.lbr", "parts": [{"package": "R_4k7", "lbrPath": "C:/tmp/newlib/R_4k7.lbr", "modelPath": "C:/tmp/newlib/R_4k7.step"}]}),
    ("fusion_capture_library_views", "Capture the LIBRARY-EDITOR views EEs trust: the schematic SYMBOL (full pinout), the FOOTPRINT (pads + layer stack), and the COMPONENT/device view (Content Manager: symbol + the package table with the footprint<->package Mapped check + pin/pad counts). The 3D before/after shots don't show these. Requires the .lbr OPEN in the Electronics Library workspace (fusion_open_lbr first). Runs EDIT <pkg>.sym/.pac/.dev + WINDOW FIT + a background hwnd screenshot per view; returns the PNG paths on the box (C:/tmp/conduit-screenshots - pull with desktop_pull_file). EDIT uses the DEVICESET name (e.g. ESP32-S3FN8).",
     {"packages": "required [<deviceset name>] (or 'package': single name)", "views": "optional subset of ['component','symbol','footprint'] (default all)", "settle": "optional float secs after each EDIT before capture (default 1.5; raise if a shot shows the PREVIOUS part)"},
     {"success": "bool", "captured": "[{package,view,path,ok}]"}, 300, _STATUS, True, {"packages": ["ESP32-S3FN8", "ATSAMD51J20A-AUT"]}),
    ("fusion_cleanup_cloud_files", "PRECISELY delete a list of cloud files by lineage urn - SAFE cleanup of AI-created clutter. Deletes ONLY the exact fileIds given (never name-guessing), so it can't touch a teammate's file in a shared folder. Loops server-side. Get the ids from fusion_aps_browse (item .id). Use this to remove f3d files the bridge created. A big list takes minutes (relay may time out at ~60s while deletes continue server-side - re-browse to confirm).",
     {"fileIds": "required [<lineage urn>]", "projectName": "optional str (default active project)", "folderPath": "optional str (default root - where the files live)"},
     {"success": "bool", "deletedCount": "int", "failedCount": "int", "failed": "[{fileId,error}]"}, 1800, _STATUS, True, {"fileIds": ["urn:adsk.wipprod:dm.lineage:abc123"], "projectName": "Adom"}),
    ("fusion_run_modeling_script", "Run an adsk.fusion modeling script in the live session (sketches/extrudes/params). Free programmatic CAD.",
     {"script": "required str (Python using adsk; globals: adsk, app, ui, design)"},
     {"result": "obj", "documentName": "str"}, 180, None, True, {"script": "import adsk.fusion\n# ...build geometry..."}),

    ("fusion_assembly_bom", "Structured, kit-aware MECHANICAL bill of materials for the active Design assembly (the counterpart to the electronics-only fusion_export_bom). Recurses organizational subassemblies but counts a physical part or a purchased kit/unit ONCE, so hardware modeled inside a kit (a '... with fasteners' bracket, a bearing/pulley) is NOT double-counted the way a flat allOccurrences walk counts it. Matches Fusion's Manage -> BOM quantities. Optionally writes a CSV.",
     {"treatAsUnit": "optional [str] name substrings counted as one unit (default ['with fasteners'])", "exclude": "optional [str] top-level name prefixes to skip", "includePhysicalProperties": "optional bool (adds volume_cm3/mass_kg per line)", "outputPath": "optional str (Windows CSV path)"},
     {"success": "bool", "design": "str", "partCount": "int", "totalInstances": "int", "parts": "[{componentName, partNumber, description, material, quantity, bodies}]"},
     120, None, False, {"treatAsUnit": ["with fasteners"], "includePhysicalProperties": True}),

    ("fusion_physical_properties", "Per-component physical properties (volume cm3, mass kg, density, area, center of mass) for the active design. Reads them directly so BOM/costing tools no longer have to run getPhysicalProperties inside a modeling script. Pass names to scope to specific components.",
     {"names": "optional [str] component names to measure (default all)", "accuracy": "optional low|medium|high|veryhigh (default low)"},
     {"success": "bool", "count": "int", "properties": "{name: {volume_cm3, mass_kg, density, area_cm2, center_of_mass}}"},
     120, None, False, {"accuracy": "low"}),

    ("fusion_set_preference", "Set Fusion appearance/navigation preferences in the LIVE session - applies immediately, NO restart. Keys: theme (light/darkblue/darkgray/classic/device/dark; some builds only ship LightGray/DarkBlue/Device, failures reported per-key), invertScrollZoom (bool - mouse scroll/zoom direction), orbitScheme (fusion360/alias/inventor/solidworks/tinkercad/powermill), modelingOrientation (yup/zup), gestureNav (bool), cameraPivot (bool), lengthUnit (mm/cm/m/in/ft - active design). Per-key outcome in `applied`, resulting state in `current`.",
     {"theme": "optional str", "invertScrollZoom": "optional bool", "orbitScheme": "optional str", "modelingOrientation": "optional str", "gestureNav": "optional bool", "cameraPivot": "optional bool", "lengthUnit": "optional str"},
     {"applied": "obj", "current": "obj"}, 30, None, True, {"theme": "dark"}),

    ("fusion_get_preferences", "Read current Fusion appearance/navigation preferences (theme, activeTheme, invertScrollZoom, orbitScheme, modelingOrientation, gestureNav, cameraPivot, lengthUnit) as friendly values - the same keys fusion_set_preference accepts.",
     {}, {"current": "obj"}, 20, None, False, {}),

    # ── CAD & 3D export ────────────────────────────────────────────────────
    ("fusion_export_step", "Export the active design to STEP (needs the 3D view).",
     {"outputPath": "required str"}, {"format": "str", "fileSizeKB": "int"}, 300, None, True, {"outputPath": "C:/out/board.step"}),
    ("fusion_export_iges", "Export to IGES.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/board.iges"}),
    ("fusion_export_stl", "Export to STL mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.stl"}),
    ("fusion_export_3mf", "Export to 3MF mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.3mf"}),
    ("fusion_export_usdz", "Export to USDZ (AR / Hydrogen 3D).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.usdz"}),
    ("fusion_export_optimized_glb", "Fusion board -> wiki-grade optimized GLB. Exports STEP (+ optional silkscreen) and submits it to service-step2glb molecule mode (anchor to MP pins + silk bake + gold pins + join/weld/prune + Draco): ~465KB/~31 draw calls vs a raw ~16MB/~25000 that halts the viewer. Bounded-waits `wait`s (default 90); small boards return complete, big boards return {pending, jobId} - finish with fusion_fetch_optimized_glb. Use for a component page's component.parts.model_3d. STACKUP RULE (John 2026-07-15, see pcb-stackup skill): board bbox = the FR4 body, NEVER the whole assembly (components overhang edges + tower above); each overlay goes at ITS layer's z (copper at FR4-top, silk above mask); copper traces are REAL 35um 3D bodies colored like the FR4 - recolor them + translucent mask in the RAW GLB before optimizing to make routing visible; put the measured stackup table on the board's wiki page. PUBLISH EVERYTHING: a board's wiki page repo must carry EVERY exportable design file (.f3d/.step/.usdz/EAGLE .brd+.sch/Fusion .fbrd+.fsch/gerbers/BOM/CPL/GLBs/renders), not just the 3D - the page exists to share the design.", {"outputPath": "required str (.glb)", "silkscreen": "bool default true", "pin": "str medium|large default medium", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "jobId": "str", "pending": "bool", "meshesAfter": "int", "sizeBytes": "int", "moleculeAnchored": "bool", "silkscreenApplied": "bool"}, 200, None, True, {"outputPath": "C:/out/board.glb"}),
    ("fusion_fetch_optimized_glb", "Finish a fusion_export_optimized_glb job: bounded-poll the optimizer by jobId and write the GLB to outputPath when complete. Re-call until status=complete (big boards tessellate a few minutes). No Fusion needed.", {"jobId": "required str", "outputPath": "str (.glb)", "wait": "int seconds default 15 (keep under the ~60s relay timeout; re-call fetch until complete)"}, {"glbPath": "str", "status": "str", "pending": "bool", "sizeBytes": "int"}, 150, None, False, {"jobId": "abc123", "outputPath": "C:/out/board.glb"}),
    ("fusion_board_stackup", "Read a board's PHYSICAL fabrication stackup (see the pcb-stackup skill): copper layer count + copper/dielectric thicknesses from the EAGLE design rules (layerSetup/mtCopper/mtIsolate) plus the measured FR4 extent. A PCB is Cu/prepreg/Cu/core/.../Cu - the FR4 is NOT one slab. Use to build the stackup table + real-thickness exploded 3D view, and put a stackup table on every exported board's wiki page.", {}, {"layerSetup": "str", "copperLayers": "list", "copperCount": "int", "copperThickness_mm": "list", "dielectricBonds": "list (prepreg/core order)", "fr4": "{x_mm,y_mm,dielectric_mm}"}, 200, None, True, {}),
    ("fusion_export_obj", "Export to OBJ mesh.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.obj"}),
    ("fusion_export_dxf", "Export 2D DXF.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dxf"}),
    ("fusion_export_dwg", "Export 2D DWG.", {"outputPath": "required str"}, {"format": "str"}, 120, None, True, {"outputPath": "C:/out/sketch.dwg"}),

    # ── Manufacturing ──────────────────────────────────────────────────────
    ("fusion_export_gerbers", "Run the bundled JLCPCB CAM job (auto 2/4-layer) to produce the gerber + drill ZIP.",
     {"outputDir": "optional str (default C:/tmp/adom-gerbers)"}, {"fileCount": "int", "files": "[{name,size}]", "zipPath": "str"}, 180, None, True, {"outputDir": "C:/tmp/jlc"}),
    ("fusion_export_bom", "Export a Bill of Materials CSV (grouped by value+package) from the open board.",
     {"outputPath": "optional str", "grouped": "optional bool (default true)"}, {"componentCount": "int", "grouped": "bool"}, 60, None, False, {"outputPath": "C:/tmp/bom.csv"}),
    ("fusion_export_cpl", "Export the Component Placement List (pick-and-place) CSV.",
     {"outputPath": "optional str", "side": "optional str (all|top|bottom)"}, {"topCount": "int", "bottomCount": "int"}, 60, None, False, {"outputPath": "C:/tmp/cpl.csv"}),
    ("fusion_set_design_rules", "Load manufacturing-grade DRC into the open board (JLCPCB 2/4-layer profiles).",
     {"action": "optional str (apply|export|show)", "layers": "optional str (auto|2|4)"}, {"activeWorkspace": "str", "description": "str"}, 90, None, False, {"action": "apply", "layers": "2"}),
    ("fusion_load_design_rules", "Load a bundled .edru design-rule set into the board.",
     {"layers": "optional str (auto|2|4)"}, {"success": "bool"}, 90, None, False, {}),
    ("fusion_detect_layers", "Detect 2-layer vs 4-layer board (ULP + CAM comparison).",
     {}, {"layerCount": "int", "copperLayers": "[int]"}, 90, None, False, {}),
    ("fusion_dismiss_blocking_dialogs", "Detect + classify (and optionally dismiss) blocking Fusion dialogs.",
     {}, {"dialogs": "[...]"}, 30, None, False, {}),
    ("fusion_electron_run", "Run an EAGLE/Electron command in the open board (the generic extension point, e.g. RUN <ulp>).",
     {"command": "required str"}, {"success": "bool", "output": "str"}, 90, None, False, {"command": "set confirm yes;RUN 'C:/tmp/jlcpcb_smta_exporter.ulp' 'C:/tmp/jlc'"}),
    ("fusion_electron_zoom", "Smoothly ZOOM the schematic/2D board for a recording (the whole motion is one call). factor>1 zooms in, <1 out; or fit:true. The loop runs in the add-in with a repaint per frame.",
     {"factor": "float (default 2.0)", "fit": "optional bool", "steps": "optional int (default 16)", "frameDelayMs": "optional int (default 35)"},
     {"success": "bool", "output": "str"}, 60, None, True, {"factor": 3, "steps": 20}),
    ("fusion_electron_pan", "Smoothly PAN + zoom the view to frame a board-coordinate box (mm; get part positions from fusion_board_info). Animates from where the view last was.",
     {"x1": "float", "y1": "float", "x2": "float", "y2": "float", "steps": "optional int (default 16)", "frameDelayMs": "optional int (default 35)"},
     {"success": "bool", "output": "str", "box": "[...]"}, 60, None, True, {"x1": 0, "y1": 0, "x2": 10, "y2": 8}),
    ("fusion_electron_select", "SELECT a part (by reference designator, or x/y board mm) and surface its properties - the way clicking a part reveals the properties panel. Great for showing life in a recording.",
     {"name": "str (refdes, e.g. R1)", "x": "optional float", "y": "optional float", "properties": "optional bool (default true)"},
     {"success": "bool", "output": "str", "deviceInfo": "str"}, 30, None, True, {"name": "U1"}),

    ("fusion_inspect_bodies", "GEOMETRY READ-BACK: what actually got built, in MILLIMETRES. Per body: bbox, sizeMm, volumeMm3, areaMm2, faceCount, cylindricalFaceCount, appearance, material, isVisible. Use this to VERIFY your own modelling - a body at the wrong Z (a double-applied offset, a part buried inside another) renders perfectly and is INVISIBLE in a screenshot, and this work is often matte-black parts on a dark canvas where pixels are a weak check. cylindricalFaceCount is the cheap way to confirm holes were actually cut: a symmetric cut that reaches nothing fails SILENTLY with no exception. Pass worldSpace:true to resolve bodies through their occurrence transforms (assembly placement); optionally filter with occurrence. Fusion's API is centimetres - this converts for you.",
     {"worldSpace": "optional bool (resolve through occurrence transforms; default false = root component only)", "occurrence": "optional str (substring filter on occurrence name, worldSpace only)"},
     {"success": "bool", "units": "str (mm)", "bodyCount": "int", "documentName": "str",
      "bodies": "[{name,owner,bbox:{x,y,z},sizeMm,volumeMm3,areaMm2,faceCount,cylindricalFaceCount,appearance,material,isVisible}]"},
     130, _STATUS, False, {"worldSpace": True}),

    # ── Self-describe ──────────────────────────────────────────────────────
    ("fusion_describe", "Self-describe every verb the bridge exposes (this list) for AD's Verbs tab + runner.",
     {}, {"success": "bool", "verbs": "[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]"}, 20, None, False, {}),
]


def _verbs():
    out = []
    for name, summary, inp, outp, timeout, status, longr, ex in _T:
        if name.endswith("_note"):
            continue
        out.append({
            "name": name,
            "summary": summary,
            "input": inp,
            "output": outp,
            "timeoutSeconds": timeout,
            "statusVerb": status,
            "longRunning": longr,
            "example": ex,
        })
    return out


def handle_describe(args: dict) -> dict:
    verbs = _verbs()
    # `verbs` at the TOP LEVEL is what AD's Verbs tab reads from the raw /command
    # response. AD's relay strips non-standard top-level fields on the CLI proxy
    # path, so we ALSO nest under `data` (which the relay forwards) for CLI/debug.
    return {
        "success": True,
        "bridge": "fusion360",
        "verbCount": len(verbs),
        "verbs": verbs,
        "data": {"bridge": "fusion360", "verbCount": len(verbs), "verbs": verbs},
    }
