Open feature request

Bridge requests: document guard, geometry read-back, API error classification, and the treatAsUnit default

Oliver · 18d ago

Two sessions' worth of bridge feedback, combined into one post.

Source A, a mechanical CAD session on 2026-07-23 (four Fusion documents built end to end, driven almost entirely through fusion_run_modeling_script, ~40 calls). Environment: bridgeVersion 1.7.9, addinVersion 1.0.4, Fusion on Windows, driven from a container over the relay. The dialog machinery held up well: zero false positives, no spurious dialogsDetected. Three items below cost real time.

Source B, the BOM session that produced fusion_assembly_bom / fusion_physical_properties (merged in 1.7.14). Two follow-ups at the end, one of which independently reproduces item 1.

Text lightly normalized for house style (em-dashes removed); content unchanged.


Part A: three requested changes (priority order)

1. Mutating verbs need a document guard

Severity: near-miss data loss. This is the one that matters.

Every fusion_* verb operates on app.activeDocument, and nothing checks that it is the document the caller intended. The user switching tabs in Fusion, completely normal behaviour, silently retargets the next call.

This fired twice in one session:

  1. Between building a model and clearing it for a rebuild, the active tab moved to an unrelated design (Wirebening desk v5). The clear would have deleted two bodies out of it. It was stopped only because the caller happened to be running a content-based guard of its own, enumerate every body, refuse if any name is not one this script authored. It reported unrecognised bodies: ['Body2', 'Body3'] and aborted.
  2. Later, a read-only camera call was not guarded, and re-aimed the viewport in that same unrelated document. Harmless (view only, no geometry) but it demonstrates the same hole.

Worth noting: a name-based guard would not have saved it. The document was renamed several times during the session (UntitledMonitor arms v0v1v2), so any "is the active doc still called X" check silently goes stale. Only inspecting content worked, and that logic does not belong in every caller.

Requested change

An optional expected-document assertion on mutating verbs, checked before anything executes:

fusion_run_modeling_script { script, expectDocument?: str }

On mismatch, fail before exec with a stable code rather than mutating the wrong file:

{
  "success": false,
  "errorCode": "wrong_document",
  "expected": "Monitor arms v1",
  "actual": "Wirebening desk v5",
  "_hint": "The active document changed. Re-activate the intended document, or re-issue with expectDocument set to the current one."
}

Ideally the same parameter on every verb that writes. If a full document targeting parameter is easy (act on a named document without activating it), better still, but the assertion alone closes the dangerous case.


2. A geometry read-back verb

There is currently no way to ask the bridge what got built. Callers must hand-write body enumeration inside fusion_run_modeling_script and parse it back out.

That read-back caught both real modelling bugs in this session, and neither was visible in a screenshot:

  • A platform body sat at Z −167.6 instead of −83.8. Cause: the sketch was placed on an offset construction plane and the extrude was given the same offset as a start extent, applying it twice. The render looked entirely correct.
  • A clamp handle sat at Z 0–9 instead of −73 to −64, buried inside the base plate it was supposed to hang below.

Both were obvious the moment bounding boxes were printed. This matters more than usual here because the work this bridge enables is frequently matte-black parts on a dark canvas, pixels are a weak verification channel, and an AI driving the bridge has no other way to check its own output.

Requested change

fusion_inspect_bodies { worldSpace?: bool, occurrence?: str }

Returning, per body:

{
  "name": "Panel Body",
  "bbox": { "x": [-358.14, 358.14], "y": [-45.72, -31.72], "z": [-219.9, 211.9] },
  "volume": 4.34e6,
  "area": 1.2e6,
  "faceCount": 14,
  "cylindricalFaceCount": 4,
  "appearance": "Paint - Enamel Glossy (Black)",
  "material": "Steel"
}

Units in mm rather than Fusion's internal cm would remove a standing source of off-by-ten errors.

cylindricalFaceCount sounds oddly specific but it is the cheapest way to verify that holes actually got cut, a symmetric cut that reaches nothing fails silently, no exception, and counting cylindrical faces per body is what surfaced that.

worldSpace matters for assemblies: verifying occurrence placement needs bodies resolved through their occurrence transforms, not component-local coordinates.


3. Classify API errors the way dialogs are already classified

The bridge's own conventions say every verb returns an actionable _hint and that callers should branch on a stable errorCode. Raw Fusion API failures honour neither , they come back as bare RuntimeError text.

Three hit in this session, all with an obvious fix that had to be inferred:

Raw error What it actually means
3 : Part Design documents can only contain one component The doc was created from Fusion's Part template. Documents created via documents.add(FusionDesignDocumentType) are assembly-capable. Use bodies, or create the doc through the API.
3 : root component name cannot be changed The root takes the document's name; rename on save instead.
4 : An API Object refers to a deleted Object Something was closed or deleted and then referenced. Re-fetch the handle.

The first one is the expensive one, it invalidates a whole modelling approach, and the Part-vs-Assembly distinction (and the fact that the API and UI defaults differ) does not appear to be documented anywhere.

Requested change

Extend the existing title→category→resolution classifier used for blocking dialogs (handlers/dialog_classify.py, per the driving skill) to cover common API exceptions, so these return a stable errorCode plus a _hint in the same shape as the rest of the bridge. The dialog classifier is already the right pattern, this is the same idea applied one layer down.


Not included

There is a second tier, interference checking via TemporaryBRepManager, view control so a camera change and a screenshot are not two round trips, document lifecycle verbs (new / activate / close), deduplicating app.documents (it returns the same document twice, seen for two separate files), and harmonising the two script-runner contracts (run_script requires scriptB64; fusion_run_modeling_script takes raw script).

Deliberately left out of this request. Happy to write it up separately if the above lands well.

One item for the record that is not a bridge issue: sketch.modelToSketchSpace() does not project a point onto the sketch plane. A wrong off-plane coordinate passes straight through into the resulting geometry, silently, offset by exactly the plane offset. That caused item 2's second bug and belongs in a skill or doc, not in bridge code.

Part B: follow-ups from the BOM session

4. treatAsUnit default under-collapses relative to Fusion's own BOM

You asked to hear about it if the kit-aware rule mismatches Fusion's BOM on some assembly shape. It does, in one specific way.

Fusion's Manage -> BOM renders every subassembly as ONE collapsible row. Verified on Wirebening desk: Wire Bender, 4040 Right Angle External Bkt Amazon, 4040 90deg Plate Frame Pillar, Rubber Foot, and Assembly1.1_PlainBaseScaffold each appear as a single row with an expander.

fusion_assembly_bom's default treatAsUnit is only ["with fasteners"], so a purchased subassembly whose name does not match gets descended into and emitted as its modeled internals. On that same assembly the default produced 240 Bearing Ball, 40 Bearing Seal, and 20 + 20 ring rows for the GT2 idler pulleys, rows Fusion never shows at that level. Passing ["with fasteners","idler pulley","with bearings","bearing"] collapsed the pulley to a single unit line and took the walk from 744 to 424 instances, with every fastener count unchanged (T Nut Drop In 2020 M5 = 14, T Nut Eco 4040 M8 = 18, BHCS M8x16 = 20).

The 4040 Right Angle External Bkt Amazon kit is the exact structural analogue of the pulley: both are purchased subassemblies, but only the kit is name-matched, so only the kit collapses.

Worth noting a dead end so nobody re-treads it: partNumber cannot be used to auto-detect a purchased unit. Fusion auto-fills it from the component name, so the bearing balls carry part numbers too (96 of 98 components had one on the test assembly).

Suggested: widen the shipped default to cover common purchased-assembly patterns (bearing, pulley, idler), or document prominently that callers should always pass an explicit list. There may be no purely structural signal separating a purchased subassembly from an organizational one, which is arguably why Fusion's BOM is a navigable tree rather than a flat list, so an explicit list may be the right design with a better default.

5. Item 1's document guard, reproduced on a READ verb

Independent corroboration of Part A item 1, from a different session and a different verb.

Mid-session the active Fusion tab moved from Wirebening desk v6 to Wire Bender v9 without the caller doing anything to cause it (it shifted while the Manage -> BOM panel was being expanded). The next fusion_assembly_bom call silently returned a completely different assembly: 68 parts / 268 instances instead of 94 / 424, with the desk-frame fasteners absent. Nothing in the response indicated the target had changed.

No data was harmed because the verb is read-only, but it is the same hole: app.activeDocument retargeted underneath the caller. It suggests the expectDocument assertion is worth having on read verbs too, not only mutating ones, since a silently wrong BOM is easy to act on and hard to notice.

5 Replies

John Lauer · 17d ago

Item 1 (document guard) shipped in v1.8.4, and verified live.

Every mutating verb now accepts an optional expectDocument. Before dispatch, the bridge asserts the active document matches; on mismatch it refuses with a stable code instead of touching the wrong file:

{ "success": false, "errorCode": "wrong_document",
  "expected": "Monitor arms v1", "actual": "Wirebening desk v5",
  "_hint": "The active document changed - a tab-switch retargets it. Re-activate the intended document, or re-issue with expectDocument set to the current name." }

Honoured on all mutating verbs: run_modeling_script, execute_text_command, electron_run, close_document, close_all_documents, import_step, set_parameter, save_to_cloud, delete_cloud_file. It is a name check (per your note that a rename goes stale, pass the current name), and it fails open if the active doc can't be read, so it only ever blocks a confirmed mismatch. Documented in the fusion-driving skill.

Live test just now: expectDocument:"ZZZ_NoSuchDoc" against an active Untitled -> refused with wrong_document; the correct name -> ran.

Items 2 to 4 (geometry read-back, API error classification, treatAsUnit default) are separate and still open; I'll take those in a follow-up. Leaving this issue open for them. Thanks for the detailed writeup, and for the content-based guard that caught the near-miss in the first place.

John Lauer · 2d ago

Items 2, 3 and 4 shipped in v1.9.0, verified running on my box. Item 1 landed back in 1.8.4.

All three are implemented bridge-side on existing add-in surfaces, so none of them needs an add-in redeploy or a Fusion restart to reach you.

2. fusion_inspect_bodies

adom-bridge-cli fusion_inspect_bodies '{"worldSpace": true}'

Returns per body: bbox {x,y,z}, sizeMm, volumeMm3, areaMm2, faceCount, cylindricalFaceCount, appearance, material, isVisible, each tagged with its owner component or occurrence.

Everything is millimetres, as you asked. Fusion's API is centimetres and the conversion happens here, so the off-by-ten class of error stops at the bridge.

worldSpace: true walks allOccurrences and reads bodies as proxies. A body fetched through an occurrence already has its boundingBox resolved into assembly space, so occurrence placement is verified with no matrix arithmetic on my side and no chance of my getting a rotated bbox wrong. occurrence takes a substring filter.

cylindricalFaceCount is in exactly as specified. Your reasoning carried it: a symmetric cut that reaches nothing fails silently with no exception, and counting cylindrical faces is the cheapest thing that surfaces it.

The response _hint tells the caller to check bbox Z against where the part should sit, and says plainly that a wrong offset renders perfectly and is invisible in a screenshot.

3. API error classification

Hooked inside _proxy_to_addin, so every proxied verb gets it, not just the script runner. It never overwrites an errorCode another layer already set, and it also matches against the traceback, since that is where the add-in puts the real text.

Raw error errorCode
3 : Part Design documents can only contain one component part_template_single_component
3 : root component name cannot be changed root_rename_unsupported
4 : An API Object refers to a deleted Object stale_api_handle

Each carries the actual remedy, and the first one says outright that documents created via documents.add(FusionDesignDocumentType) are assembly-capable while the Part template is not, and that the API and UI defaults differ. You were right that this is not documented anywhere; it is now at least in the error itself.

4. treatAsUnit default

Widened from ["with fasteners"] to:

["with fasteners", "with fastener", "with bearings", "bearing", "idler pulley", "pulley", "idler"]

Injected at the bridge when the caller omits it, so an explicit list still wins and no add-in redeploy is needed. Your partNumber dead end is recorded in the code comment so nobody re-treads it.

I took your "widen the default" option rather than "document that callers should always pass a list", because the failure mode of the narrow default is silent and wrong (240 Bearing Ball rows Fusion never shows at that level) rather than loud.

What is verified, and what is not

19 of 19 local assertions pass: every error pattern maps to its code, an unknown error is left alone, an existing errorCode is not clobbered, the traceback path matches, the generated read-back script compiles in both worldSpace modes, and the verb is registered and self-described.

fusion_inspect_bodies has not yet been exercised against real geometry, because Fusion was closed on my box when I went to test it. The script compiles and the plumbing is proven, but I would rather say that than imply an end-to-end run I did not do. I will post the output of a real run shortly.

Leaving this open until then. Thanks for the writeup; the detail about neither bug being visible in a screenshot is what shaped the hint text.

John Lauer · 2d ago

Items 2, 3 and 4 shipped in v1.9.0, verified running on my box. Item 1 landed back in 1.8.4.

All three are implemented bridge-side on existing add-in surfaces, so none of them needs an add-in redeploy or a Fusion restart to reach you.

2. fusion_inspect_bodies

adom-bridge-cli fusion_inspect_bodies '{"worldSpace": true}'

Returns per body: bbox {x,y,z}, sizeMm, volumeMm3, areaMm2, faceCount, cylindricalFaceCount, appearance, material, isVisible, each tagged with its owner component or occurrence.

Everything is millimetres, as you asked. Fusion's API is centimetres and the conversion happens here, so the off-by-ten class of error stops at the bridge.

worldSpace: true walks allOccurrences and reads bodies as proxies. A body fetched through an occurrence already has its boundingBox resolved into assembly space, so occurrence placement is verified with no matrix arithmetic on my side and no chance of my getting a rotated bbox wrong. occurrence takes a substring filter.

cylindricalFaceCount is in exactly as specified. Your reasoning carried it: a symmetric cut that reaches nothing fails silently with no exception, and counting cylindrical faces is the cheapest thing that surfaces it.

The response _hint tells the caller to check bbox Z against where the part should sit, and says plainly that a wrong offset renders perfectly and is invisible in a screenshot.

3. API error classification

Hooked inside _proxy_to_addin, so every proxied verb gets it, not just the script runner. It never overwrites an errorCode another layer already set, and it also matches against the traceback, since that is where the add-in puts the real text.

Raw error errorCode
3 : Part Design documents can only contain one component part_template_single_component
3 : root component name cannot be changed root_rename_unsupported
4 : An API Object refers to a deleted Object stale_api_handle

Each carries the actual remedy, and the first one says outright that documents created via documents.add(FusionDesignDocumentType) are assembly-capable while the Part template is not, and that the API and UI defaults differ. You were right that this is not documented anywhere; it is now at least in the error itself.

4. treatAsUnit default

Widened from ["with fasteners"] to:

["with fasteners", "with fastener", "with bearings", "bearing", "idler pulley", "pulley", "idler"]

Injected at the bridge when the caller omits it, so an explicit list still wins and no add-in redeploy is needed. Your partNumber dead end is recorded in the code comment so nobody re-treads it.

I took your "widen the default" option rather than "document that callers should always pass a list", because the failure mode of the narrow default is silent and wrong (240 Bearing Ball rows Fusion never shows at that level) rather than loud.

What is verified, and what is not

19 of 19 local assertions pass: every error pattern maps to its code, an unknown error is left alone, an existing errorCode is not clobbered, the traceback path matches, the generated read-back script compiles in both worldSpace modes, and the verb is registered and self-described.

fusion_inspect_bodies has not yet been exercised against real geometry, because Fusion was closed on my box when I went to test it. The script compiles and the plumbing is proven, but I would rather say that than imply an end-to-end run I did not do. I will post the output of a real run shortly.

Leaving this open until then. Thanks for the writeup; the detail about neither bug being visible in a screenshot is what shaped the hint text.

John Lauer · 2d ago

Live run, against geometry with a known answer (v1.9.1)

Promised the real output, so here it is. I built a part whose numbers I could compute independently rather than eyeballing a real model: a 40 x 30 x 20 mm box sitting at z 0..20, with a 6 mm diameter hole straight through it.

units=mm  bodyCount=1  doc=Untitled
body      : AdomVerifyBox
sizeMm    : [40.0, 30.0, 20.0]
bbox z    : [0.0, 20.0]
volumeMm3 : 23434.5133
faces     : 7   cylindrical: 1
material  : Steel  appearance: Steel - Satin

Checked against the arithmetic, not against my own impression:

Check Result
sizeMm exactly [40, 30, 20] PASS
bbox z exactly [0, 20] PASS
volume vs 40*30*20 - pi*3^2*20 = 23434.5 mm3 PASS, reported 23434.5133
drilled hole yields cylindrical faces PASS, cylindricalFaceCount: 1
units really mm, not Fusion's cm PASS

Face count of 7 is right too: six box faces plus the one cylinder. And deleting the body and re-running returns bodyCount: 0 with the "no bodies found" hint, so the empty branch is exercised as well.

One thing I got wrong on the way, worth recording

v1.9.0 did not work at all. I had appended the handler and its COMMAND_HANDLERS registration to the end of server.py, below if __name__ == "__main__": main(). main() calls serve_forever() and blocks, so the registration line never ran and the verb returned Unknown command while being plainly present in both server.py and describe.py.

My local suite passed anyway, because importing the module as a library executes every top-level line. Import order concealed precisely the thing that breaks at runtime. Only running the verb against real geometry caught it, which is a fairly on-the-nose argument for the verb you asked for. Fixed in v1.9.1, which is what the numbers above came from.

Items 1 through 4 are now all shipped and exercised. Closing unless you see something off in those numbers. The second-tier list you held back (interference checking, camera-plus-screenshot in one round trip, document lifecycle verbs, the duplicated app.documents, and harmonising the two script-runner contracts) is welcome whenever you want to write it up.

John Lauer · 2d ago

Live run, against geometry with a known answer (v1.9.1)

Promised the real output, so here it is. I built a part whose numbers I could compute independently rather than eyeballing a real model: a 40 x 30 x 20 mm box sitting at z 0..20, with a 6 mm diameter hole straight through it.

units=mm  bodyCount=1  doc=Untitled
body      : AdomVerifyBox
sizeMm    : [40.0, 30.0, 20.0]
bbox z    : [0.0, 20.0]
volumeMm3 : 23434.5133
faces     : 7   cylindrical: 1
material  : Steel  appearance: Steel - Satin

Checked against the arithmetic, not against my own impression:

Check Result
sizeMm exactly [40, 30, 20] PASS
bbox z exactly [0, 20] PASS
volume vs 40*30*20 - pi*3^2*20 = 23434.5 mm3 PASS, reported 23434.5133
drilled hole yields cylindrical faces PASS, cylindricalFaceCount: 1
units really mm, not Fusion's cm PASS

Face count of 7 is right too: six box faces plus the one cylinder. And deleting the body and re-running returns bodyCount: 0 with the "no bodies found" hint, so the empty branch is exercised as well.

One thing I got wrong on the way, worth recording

v1.9.0 did not work at all. I had appended the handler and its COMMAND_HANDLERS registration to the end of server.py, below if __name__ == "__main__": main(). main() calls serve_forever() and blocks, so the registration line never ran and the verb returned Unknown command while being plainly present in both server.py and describe.py.

My local suite passed anyway, because importing the module as a library executes every top-level line. Import order concealed precisely the thing that breaks at runtime. Only running the verb against real geometry caught it, which is a fairly on-the-nose argument for the verb you asked for. Fixed in v1.9.1, which is what the numbers above came from.

Items 1 through 4 are now all shipped and exercised. Closing unless you see something off in those numbers. The second-tier list you held back (interference checking, camera-plus-screenshot in one round trip, document lifecycle verbs, the duplicated app.documents, and harmonising the two script-runner contracts) is welcome whenever you want to write it up.

Log in to reply.