← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.129\ No newline at end of file+1.9.130\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.129",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.145/adom-bridge-fusion-v1.9.129.zip",-  "sha256": "8185b0cdaf6412ee50200edad0e7971a140341cf5aa59abe1bbcdbfc4ce34ce2",-  "size": 464119,+  "version": "1.9.130",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.146/adom-bridge-fusion-v1.9.130.zip",+  "sha256": "a01c28c9da36414cb2ae1e8854ecc144a0f00da33987f5fc7da6cca5f6be7b96",+  "size": 465935,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.129",+  "version": "1.9.130",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
describe.py+1
@@ -14,6 +14,7 @@ _STATUS = "fusion_get_app_state" # Compact source table: name -> (summary, input, output, timeout, statusVerb, longRunning, example) _T = [     # ── Lifecycle ──────────────────────────────────────────────────────────+    ("fusion_preflight_lbr", "PRE-ANALYZE a .lbr: will Fusion open it? Offline and instant - no Fusion needed, nothing launched. Runs the named checks the bridge learned from real Fusion rejections: file-readable, size-sane (truncated-transfer corpse detection), xml-parses, root-is-eagle, packages-nonempty (Fusion's silent 'No Components'), symbols-present, devicesets-present (missing ones make EDIT raise Create-new prompts), connects-resolve (dead wiring renders fine and connects nothing). fusion_open_lbr and fusion_create_library run the same gate automatically; call this directly to gate a batch BEFORE staging.", {"libraryPath": "str (Windows .lbr path)"}, {"success": "bool", "preflight": "{ok, checks[], errors[]}"}, 10, None, True, {"libraryPath": "C:/Temp/PART.lbr"}),     ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.", {"libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),     ("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, {}),
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.145",+  "version": "1.9.146",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+10−7
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.145",+  "version": "1.9.146",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -70,7 +74,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -99,9 +106,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }+  ] }\ No newline at end of file
server.py+110−21
@@ -3005,34 +3005,35 @@ def _orchestrate_open_lbr(args: dict) -> dict:     # its blocking "has errors and cannot be opened" Error modal ON EVERY RETRY and left     # an empty Untitled design behind each time. The file's validity is knowable offline     # in milliseconds - an unparseable library must NEVER reach Fusion's parser.-    # NOTE the _ospre alias: this function has a LOCAL `import os` further down, which-    # makes `os` local to the WHOLE scope - the 1.9.128 guard referenced bare `os` here,-    # crashed with UnboundLocalError on EVERY open, and reported its own crash as "not a-    # parseable EAGLE library" (caught by the matrix re-run within the hour, issue #47).+    # PRE-ANALYZE before Fusion ever sees the file (John, 2026-08-21; the 77-byte+    # ads1220irvar corpse raised Fusion's blocking Error modal on every retry). Only the+    # HARD opens are gated here - a parseable library with soft findings (empty+    # devicesets etc.) still opens, because open_lbr's contract is opening, but a file+    # Fusion's parser will refuse never leaves this function. NOTE the _ospre alias:+    # this function has a LOCAL `import os` further down that makes `os` local to the+    # whole scope - referencing it bare here was the 1.9.128 UnboundLocalError.     import os as _ospre-    try:-        _pf = file_path.replace("\\", "/")-        if _pf and _ospre.path.exists(_pf):-            import xml.etree.ElementTree as _ETpre-            _rootpre = _ETpre.parse(_pf).getroot()-            if _rootpre.tag != "eagle":-                raise ValueError("root element is <%s>, not <eagle>" % _rootpre.tag)-    except Exception as _epre:+    _pfr = _preflight_lbr(file_path)+    _hard = {c["check"]: c for c in _pfr["checks"]}+    if not all(_hard.get(k, {}).get("ok", True)+               for k in ("file-readable", "size-sane", "xml-parses", "root-is-eagle")):+        _pff = file_path.replace("\\", "/")         try:-            _sz = _ospre.path.getsize(_pf)-            _head = open(_pf, "rb").read(48)+            _sz = _ospre.path.getsize(_pff)+            _head = open(_pff, "rb").read(48)         except Exception:             _sz, _head = None, b""         return {"success": False, "errorCode": "lbr_invalid",-                "error": "%s is not a parseable EAGLE library (%s) - refusing to hand it to "-                         "Fusion, whose only response is a blocking Error modal plus an "-                         "orphaned empty design." % (file_path, str(_epre)[:120]),+                "preflight": _pfr,+                "error": "%s will not open in Fusion: %s - refusing to hand it over (the "+                         "only possible outcome is a blocking Error modal plus an orphaned "+                         "empty design)." % (file_path, "; ".join(_pfr["errors"])[:240]),                 "fileSize": _sz,                 "fileHead": repr(_head[:32]),-                "_hint": ("The file on THIS box is bad (size %s bytes). Re-stage it from the "-                          "source and compare sizes/hashes - a transfer that truncates or "-                          "re-encodes is the usual culprit. Nothing was opened and no dialog "-                          "was raised." % _sz)}+                "_hint": ("The file on THIS box is bad. Re-stage it from the source and "+                          "compare hashes - a transfer that truncates or re-encodes is the "+                          "usual culprit. fusion_preflight_lbr re-runs these checks in "+                          "isolation. Nothing was opened and no dialog was raised.")}     # PRE-CLOSE a stale dirty copy of this same library (deterministic-close rule): opening     # over an unsaved same-name document is how duplicate docs piled up (15 at peak) and how     # later closes rained Save-As prompts. If a dirty copy is open, resolve it NOW - it is@@ -9630,6 +9631,81 @@ COMMAND_HANDLERS["prove_library"] = _handle_prove_library   +def _preflight_lbr(path: str) -> dict:+    """PRE-ANALYZE a .lbr: will Fusion open it? Every check is offline and named.++    John, 2026-08-21: "what if you just pre-analyze a library to make sure its going to+    open ok?" - this is that, as one reusable gate. It encodes every rejection Fusion has+    actually produced on this bridge: a truncated binary file (77-byte ads1220irvar, the+    blocking Error modal), empty <packages> (Fusion's silent "No Components"), dead+    <connects> (renders fine, wires nothing), and absent devicesets (EDIT prompts).+    Returns {ok, checks: [{check, ok, detail}], errors: [...]} - callers show the list.+    """+    import xml.etree.ElementTree as _ET+    checks = []+    def _c(name, ok, detail=""):+        checks.append({"check": name, "ok": bool(ok), "detail": detail})+        return ok+    p = (path or "").replace("\\", "/")+    try:+        sz = os.path.getsize(p)+    except Exception as e:+        _c("file-readable", False, str(e)[:120])+        return {"ok": False, "checks": checks,+                "errors": [c["check"] + ": " + c["detail"] for c in checks if not c["ok"]]}+    _c("file-readable", True, "%d bytes" % sz)+    if not _c("size-sane", sz >= 200,+              "%d bytes - a real EAGLE library is never this small (truncated transfer?)" % sz):+        pass+    root = None+    if sz >= 200:+        try:+            root = _ET.parse(p).getroot()+            _c("xml-parses", True)+        except Exception as e:+            _c("xml-parses", False, str(e)[:160])+    if root is not None:+        _c("root-is-eagle", root.tag == "eagle", "root is <%s>" % root.tag)+        pkgs = [x.get("name") for x in root.iter("package")]+        syms = [x.get("name") for x in root.iter("symbol")]+        devs = [x.get("name") for x in root.iter("deviceset")]+        _c("packages-nonempty", bool(pkgs),+           "%d package(s)" % len(pkgs) if pkgs else+           "EMPTY <packages> - Fusion opens this to a silent 'No Components' library")+        _c("symbols-present", bool(syms), "%d symbol(s)" % len(syms))+        _c("devicesets-present", bool(devs),+           "%d deviceset(s)" % len(devs) if devs else+           "no <deviceset> - EDIT <name>.dev will raise Fusion's Create-new prompt")+        if pkgs and syms:+            conn = _check_connects(p)+            _c("connects-resolve", bool(conn.get("ok")),+               ("%d connect(s), all resolving" % (conn.get("connects") or 0))+               if conn.get("ok") else str(conn.get("why") or "invalid")[:160])+    ok = all(c["ok"] for c in checks)+    return {"ok": ok, "checks": checks,+            "errors": [c["check"] + ": " + c["detail"] for c in checks if not c["ok"]]}+++def _handle_preflight_lbr(fusion_info: dict, args: dict) -> dict:+    """Verb wrapper: will this .lbr open cleanly in Fusion? Offline, instant, no Fusion."""+    p = (args.get("libraryPath") or args.get("filePath") or "").replace("\\", "/")+    if not p:+        return {"success": False, "error": "libraryPath required."}+    pf = _preflight_lbr(p)+    return {"success": pf["ok"], "preflight": pf, "libraryPath": p,+            "errorCode": None if pf["ok"] else "lbr_preflight_failed",+            "_hint": ("Every check green - Fusion will open this library cleanly."+                      if pf["ok"] else+                      "Fix the named check(s) BEFORE any Fusion verb touches this file - "+                      "each one maps to a real Fusion failure (blocking Error modal, silent "+                      "No Components, Create-new prompt, or dead wiring). Re-stage and "+                      "hash-compare if size-sane or xml-parses failed: transfer corruption "+                      "is the usual cause.")}+++COMMAND_HANDLERS["preflight_lbr"] = _handle_preflight_lbr++ def _kicad_ingest_unavailable_error() -> dict:     """The shape-B refusal, shared by import_kicad and the create_library fast-fail. @@ -9908,6 +9984,19 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:     # Knowable up front, so refused up front; fusion_readiness carries the same flag.     if (sym_p or mod_p) and not _find_adom_lbr():         return _kicad_ingest_unavailable_error()+    # SHAPE A PRE-ANALYSIS at the same front door (John, 2026-08-21: "pre-analyze a+    # library to make sure its going to open ok"): a bad .lbr must never cost the caller+    # a Fusion LAUNCH to discover. Same named checks as fusion_preflight_lbr.+    if lbr_p and not (sym_p or mod_p):+        _pf = _preflight_lbr(lbr_p)+        if not _pf["ok"]:+            return {"success": False, "errorCode": "lbr_preflight_failed",+                    "preflight": _pf,+                    "error": "Pre-analysis says Fusion will NOT open this library: " ++                             "; ".join(_pf["errors"])[:400],+                    "_hint": "Nothing was launched and no dialog was raised. Fix the named "+                             "checks (fusion_preflight_lbr re-runs them in isolation), "+                             "re-stage with a hash compare, then re-call."}      import uuid as _uuid     run_id = _uuid.uuid4().hex[:12]