← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.227\ No newline at end of file+1.9.228\ 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.227",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.227/adom-bridge-fusion-v1.9.227.zip",-  "sha256": "fdee5acb7457e5244f0b785de68f8b60ae4b263be7f30c6d29c6d15444ccc96c",-  "size": 522566,+  "version": "1.9.228",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.228/adom-bridge-fusion-v1.9.228.zip",+  "sha256": "bf1ab587b0cf1ebcf176ffa60e8464cb9fac0c8f4dbf74782b7226d3cb090393",+  "size": 524308,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.227",+  "version": "1.9.228",   "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.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.9.227"+version = "1.9.228" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.227"+version = "1.9.228" edition = "2021"  [dependencies]
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.227"+DASH_VERSION="1.9.228" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.227",+  "version": "1.9.228",   "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+11−8
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.227",+  "version": "1.9.228",   "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",@@ -75,7 +79,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.",@@ -104,9 +111,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+96
@@ -3405,9 +3405,19 @@ def _orchestrate_open_lbr(args: dict) -> dict:     # Remember it so a later capture/EDIT can pre-check names offline instead of letting     # Fusion raise "Create new footprint 'X'?" and dismissing it after the fact.     _remember_open_library(file_path)+    # BUBBLE THE PART-NUMBER GAP UP WITH THE IMPORT (John, 2026-08-28). The import is NOT+    # blocked by missing part numbers - a library without them is still a real library and+    # the user asked for it - but the gap has to reach the top (the wiki's quick actions+    # UI, or web control) so they see "2 of my 5 components had no Mouser part number" and+    # can go fix it where the data lives. We are the only thing that has read the file.+    try:+        _pm = _part_metadata_report(file_path)+    except Exception:+        _pm = None     response = {         "success": True,         "output": f"Opened library: {os.path.basename(file_path)}",+        "partMetadata": _pm,         "data": {"results": results, "filePath": file_path},         "_hint": (             "Library opened in the Electronics Library editor (Content Manager). "@@ -11280,6 +11290,82 @@ def _lint_part_metadata(path: str) -> dict:     return {"ok": not warns, "warnings": warns, "totals": md["totals"]}  +def _part_metadata_report(path: str) -> dict:+    """A SHORT, user-facing metadata report an import can carry all the way up the chain.++    John, 2026-08-28: importing must NOT be blocked by missing part numbers, but the gap has+    to "bubble up all the way to the top, like in the wiki quick actions UI or via web+    control so the user sees 'oh, 2 of my 5 components had no mouser part number' so they+    can go fix it". The bridge is the end of the chain and cannot invent the data - but it+    is the only thing that has actually read the file, so it is the only thing that can say+    what is missing. Silence here means the user finds out at ordering time.++    Counts are per COMPONENT (deviceset), because that is the unit a user thinks in, with+    per-variant detail underneath for the automotive-vs-baseline case.+    """+    md = _library_part_metadata(path)+    if not md.get("ok"):+        return {"ok": None, "unavailable": md.get("error", "could not read the library")}+    lint = _lint_part_metadata(path)+    # TWO SEVERITIES, and conflating them made a GOOD library look broken (caught by my own+    # test): a part needs an MPN and a manufacturer, and it needs AT LEAST ONE distributor -+    # not all three. A library with DigiKey on one variant and Mouser on another is+    # perfectly orderable, and reporting "no LCSC part number" as a deficiency there is+    # noise that trains people to ignore the whole report.+    #   missing            -> actionable: cannot order this component+    #   absentDistributors -> informational: which catalogues lack a number, John's+    #                         "2 of my 5 components had no mouser part number"+    dist = ("MOUSER_PN", "DIGIKEY_PN", "LCSC_PN")+    fields = ("MPN", "MANUFACTURER") + dist+    comps, missing_counts = [], {f: 0 for f in fields}+    n_no_distributor = 0+    for ds in md["devicesets"]:+        vs = ds["variants"] or []+        # A component is missing a field when NO variant of it carries that field.+        absent = [f for f in fields if not any(v["attributes"].get(f) for v in vs)]+        for f in absent:+            missing_counts[f] += 1+        blocking = [f for f in ("MPN", "MANUFACTURER") if f in absent]+        if all(d in absent for d in dist):+            blocking.append("a distributor part number")+            n_no_distributor += 1+        if blocking or [d for d in dist if d in absent]:+            comps.append({"component": ds["name"], "variants": len(vs),+                          "missing": blocking,+                          "absentDistributors": [d for d in dist if d in absent],+                          "variantNames": [v["fullName"] for v in vs]})+    total = len(md["devicesets"])+    label = {"MPN": "no manufacturer part number", "MANUFACTURER": "no manufacturer",+             "MOUSER_PN": "no Mouser part number", "DIGIKEY_PN": "no DigiKey part number",+             "LCSC_PN": "no LCSC part number"}++    def _n(n, what):+        return "%d of %d component%s ha%s %s" % (n, total, "" if total == 1 else "s",+                                                 "s" if n == 1 else "ve", what)+    bits = [_n(missing_counts[f], label[f]) for f in ("MPN", "MANUFACTURER")+            if missing_counts[f]]+    if n_no_distributor:+        bits.append(_n(n_no_distributor, "no distributor part number at all"))+    return {+        "ok": lint["ok"],+        "componentsTotal": total,+        "variantsTotal": md["totals"]["variants"],+        "missingCounts": missing_counts,+        "componentsWithoutAnyDistributor": n_no_distributor,+        "distributorCoverage": {d: total - missing_counts[d] for d in dist},+        "components": comps[:40],+        "warnings": lint["warnings"],+        "summary": ("; ".join(bits) + ". The library imported fine - this is about ordering, "+                    "not geometry." ) if bits else+                   "All %d component(s) carry a manufacturer part number and at least one "+                   "distributor part number." % total,+        "distributorNote": ("; ".join(_n(missing_counts[d], label[d]) for d in dist+                                      if missing_counts[d]) or None),+        "userAction": ("Add the missing part numbers where the component metadata lives "+                       "(upstream of this bridge), then re-export." if bits else None),+    }++ def _handle_library_parts(fusion_info: dict, args: dict) -> dict:     """Every ORDERABLE variant in a .lbr with its part numbers. Offline, no Fusion needed. @@ -11887,6 +11973,16 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:                 state.update(stage="done", success=bool(prove.get("proven")),                              verdict=("proven" if prove.get("proven") else "not_proven"),                              evidenceCount=_frames, prove=prove)+            # The part-number gap rides the RUN STATE, so web control sees it on the same+            # poll it already makes rather than needing a second call (John, 2026-08-28).+            # Attached on the failure path too: a chain that could not prove the geometry+            # still read the file, and the user still wants to know what is missing.+            try:+                _lbr_for_meta = state.get("libraryPath") or target_lbr+                if _lbr_for_meta:+                    state["partMetadata"] = _part_metadata_report(_lbr_for_meta)+            except Exception:+                pass             _save()         except Exception as e:             state.update(stage="failed", success=False, verdict="not_proven",
skills/fusion-part-metadata/SKILL.md+46
@@ -121,3 +121,49 @@ lives. - Libraries this bridge *generates* from bare geometry emit `<technology name=""/>` with no   attributes. That is correct for a geometry-only wrapper and **wrong** to ship as a real   part: add the attributes before it reaches a user.++## Missing part numbers NEVER block the import — they bubble up++John, 2026-08-28: *"i think its ok to continue to import a library if it doesn't have this+data, but you should report it upstream so it bubbles up all the way to the top."*++So the import succeeds and the gap travels with it. `fusion_open_lbr` returns+`partMetadata`, and `fusion_create_library` puts the same block in its run state, so+`fusion_prove_result` carries it on the poll web control already makes — no second call:++```json+"partMetadata": {+  "ok": false,+  "componentsTotal": 5,+  "summary": "2 of 5 components have no distributor part number at all.+              The library imported fine - this is about ordering, not geometry.",+  "distributorNote": "2 of 5 components have no Mouser part number; ...",+  "distributorCoverage": { "MOUSER_PN": 3, "DIGIKEY_PN": 0, "LCSC_PN": 0 },+  "components": [ { "component": "U1", "missing": ["a distributor part number"],+                    "absentDistributors": ["MOUSER_PN","DIGIKEY_PN","LCSC_PN"] } ],+  "userAction": "Add the missing part numbers where the component metadata lives+                 (upstream of this bridge), then re-export."+}+```++### Two severities, and do not conflate them++| Field | Severity | Meaning |+|---|---|---|+| `missing` / `summary` / `ok` | **Actionable** | No MPN, no manufacturer, or **no distributor at all** — this component cannot be ordered |+| `absentDistributors` / `distributorNote` / `distributorCoverage` | **Informational** | *Which* catalogues lack a number |++A part needs an MPN, a manufacturer, and **at least one** distributor — not all three. A+library with DigiKey on one variant and Mouser on another is perfectly orderable; calling+that a deficiency is noise that trains people to ignore the whole report. (I shipped exactly+that bug for one commit and my own test caught it: a complete library reported `ok: true`+while the summary nagged about LCSC.)++Render `summary` as the headline and `distributorNote` as the detail line. `userAction` is+`null` when there is nothing to fix, so a clean library stays quiet.++### The bridge cannot fix this, only report it++We are the end of the chain. If a component's metadata lacks part numbers, they are missing+upstream where the component is defined — surface the names and let the fix happen where the+data lives. Never invent a part number.