← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.128\ No newline at end of file+1.9.129\ 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.128",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.144/adom-bridge-fusion-v1.9.128.zip",-  "sha256": "cf3902dc227eaeaddb8d393b0d6a178f4b31f0cbd67506b9ac252ac549ddd539",-  "size": 463809,+  "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,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.128",+  "version": "1.9.129",   "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.",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.144",+  "version": "1.9.145",   "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.144",+  "version": "1.9.145",   "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+11−2
@@ -3005,16 +3005,21 @@ 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).+    import os as _ospre     try:         _pf = file_path.replace("\\", "/")-        if _pf and os.path.exists(_pf):+        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:         try:-            _sz = os.path.getsize(_pf)+            _sz = _ospre.path.getsize(_pf)             _head = open(_pf, "rb").read(48)         except Exception:             _sz, _head = None, b""@@ -10200,6 +10205,10 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) ->   def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = None) -> dict:+    import time as _time  # MUST be first: a later local `import time as _time` makes the+    # name local to this whole scope, so three earlier uses raised UnboundLocalError -+    # found by tools/shadow_check.py minutes after the same class shipped as the 1.9.128+    # open_lbr regression (issue #47). The gate now runs at every release.     """Dispatch a command to the appropriate handler.      caller_identity: {'thread','container','reason'} from the X-Adom-Caller-* headers AD stamped on
tools/shadow_check.pyadded+56
@@ -0,0 +1,56 @@+"""Ship gate: catch local imports that shadow a module name USED EARLIER in the function.++The 1.9.128 regression in one sentence: a guard at the top of _orchestrate_open_lbr used+`os.path.exists`, a `import os` 150 lines later made `os` local to the whole scope, and+every open crashed with UnboundLocalError - reported as the caller's file being corrupt.+The matrix lost a full run to it within the hour (issue #47).++Run over any file(s); exits 1 with findings, 0 clean. Wired next to the AST parse in the+release ritual - same class of static check, same moment.+"""+import ast+import sys+++def check(path: str) -> list:+    tree = ast.parse(open(path, encoding="utf-8").read())+    findings = []+    for fn in ast.walk(tree):+        if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):+            continue+        # names bound by LOCAL imports inside this function (not nested functions)+        local_imports = {}   # name -> lineno of the import+        for node in ast.walk(fn):+            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node is not fn:+                continue+            if isinstance(node, ast.Import):+                for a in node.names:+                    k = a.asname or a.name.split(".")[0]+                    local_imports[k] = min(local_imports.get(k, node.lineno), node.lineno)+            elif isinstance(node, ast.ImportFrom):+                for a in node.names:+                    k = a.asname or a.name+                    local_imports[k] = min(local_imports.get(k, node.lineno), node.lineno)+        if not local_imports:+            continue+        # any LOAD of one of those names BEFORE its import line = UnboundLocalError at runtime+        for node in ast.walk(fn):+            if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):+                imp_line = local_imports.get(node.id)+                if imp_line and node.lineno < imp_line:+                    findings.append(+                        "%s:%d: `%s` used in %s() before its local import on line %d "+                        "- the local import makes it local to the WHOLE function, so "+                        "this line raises UnboundLocalError" % (+                            path, node.lineno, node.id, fn.name, imp_line))+    return findings+++if __name__ == "__main__":+    all_findings = []+    for p in sys.argv[1:] or ["server.py"]:+        all_findings += check(p)+    for f in all_findings:+        print(f)+    print("%d shadowing finding(s)" % len(all_findings))+    sys.exit(1 if all_findings else 0)