# CLAUDE.md - adom-desktop-fusion-bridge (READ THIS, it is YOUR project)

This repo is **the Adom Desktop Fusion 360 bridge**. If you are an Adom AI touching anything
Fusion, **you own and maintain this code.** You write it, version it, publish it, and reinstall it.
Stop treating bridge problems as someone else's. (This file exists because an AI repeatedly forgot
all of this and filed its own bridge bugs as "adom-desktop" requests. Don't.)

## You vs adom-desktop (AD) core - the boundary

- **AD core owns:** the `adom-desktop` CLI, the relay / command passthrough, bridge lifecycle
  (`bridge_install` / streaming the bridge from the wiki into `…\bridges-cache\fusion360`), and the
  core `desktop_*` verbs. That's it. AD just **hosts and loads** this bridge.
- **YOU (this bridge) own:** every `fusion_*` verb and its response `_hint`s, the **APS** integration
  (auth/config/quota - see below), the **Fusion add-in** code AND **deploying it into Fusion**, the
  `SKILL.md` + bundled `skills/`, and the edit → publish → reinstall loop.
- So: a `fusion_*` verb behaves wrong, a hint is missing, APS misbehaves, the add-in is stale → it's
  **your code**, fix it here and republish. It is NOT an adom-desktop request.

## Map of what you manage

- `server.py` - bridge server / verb router (talks to AD's relay, proxies to the add-in). **On start
  it calls `install_addin()`** to deploy the add-in (see deploy below).
- `aps.py` - the **entire APS** implementation: PKCE OAuth, token store, server-indexed cloud search,
  never-charge cap. **All APS is yours** (the `fusion_aps_*` verbs + `fusion_search_cloud_files` /
  `fusion_walk_cloud_tree`). NOTE: port **8910 is AD's vestigial "native" APS** - NOT yours; this
  bridge uses **8917/8918/8920** and steers around it. The bare `aps_status` (8910) is that AD stub,
  not this bridge's `fusion_aps_status`.
- `addin/AdomBridge/` - the add-in that runs **inside Fusion**: `AdomBridge.py`, `http_server.py`,
  `commands/*.py` (cloud_documents, open_electronics_file, electronics, export, modeling,
  manufacturing, screenshot, document_info, parameters, …).
- `describe.py` - the `fusion_describe` self-describe catalog (AD's Verbs tab).
- `handlers/` - close / recovery / dialog-classify / UI / open_design.
- `skills/` - bundled skills you ship: `fusion-aps-search`, `fusion-aps-signin`, `fusion-bridge-dev`,
  `fusion-onboarding`. `resources/` - JLCPCB CAM jobs, Adom DRU rules, the layer-detect ULP.
- `bridge.json` + `BRIDGE_VERSION` - the manifest/version (keep in lockstep).

## How you DEPLOY when you change your code (VERIFIED by watching v1.5.1 ship, 2026-06-28)

Fusion loads the add-in from a PER-USER add-in dir that Autodesk has MOVED across versions:
**2025+ Fusion scans `%APPDATA%\Roaming\Autodesk\FusionAddins\AdomBridge\`**; older builds used
`%APPDATA%\Roaming\Autodesk\Autodesk Fusion[ 360]\API\AddIns\AdomBridge\`. An add-in in the
wrong dir is SILENTLY ignored (Fusion runs fine, port 8774 never comes up - issue #63, root-caused
live). `install_addin.py` therefore installs to ALL of them (TARGET_CANDIDATES); NEVER assume a
single path, and if a future Fusion stops loading the add-in, FIRST suspect the dir moved again
(hunt for new dirs under %APPDATA%\Autodesk yourself). NOT from this repo or the cache.
⛔ NEVER ask the user to restart Fusion or enable the add-in - YOU restart (fusion_stop +
fusion_start); runOnStartup does the rest. The exact, watched-it-happen release sequence (AFTER publishing to the wiki):

1. **`bridge_install {"manifestUrl": ".../adom-bridge-fusion-manifest.json"}`** - streams the new zip
   into the CACHE via `method: in_place_merge`. Updates the cache ONLY; it does NOT touch Fusion's
   AddIns dir. (Pass `"force": true` to re-merge when the version already bumped.)
   Do NOT manually `bridge_kill` - `bridge_install`'s own output says "AD reaps + respawns it from the
   new cache on the next call (no manual kill needed)," so the bridge SERVER (new server.py/describe.py)
   reloads automatically on the next verb. (Running `bridge_kill` here is pointless.)
2. **`fusion_stop`** (graceful; `fusion_kill` if it is wedged) - REQUIRED. Fusion holds the add-in files
   OPEN, so the sync canNOT overwrite them while Fusion runs. (This is why a bridge-server respawn alone
   does NOT update the add-in, and why `fusion_stop`+`fusion_start` by itself deploys nothing.)
3. Deploy the add-in into Roaming. Two ways:
   - **(intended)** Run the cache's `install_addin.py`: `shell_execute` -> `cd <cache>\fusion360 &&
     python install_addin.py`. It prints `Updated: commands\cloud_documents.py ...`.
   - **⚠️ TRAP (hit 2026-06-29): `python`/`py` are NOT on the box's shell PATH.** AD spawns the bridge
     with its OWN python, so `python install_addin.py` via `shell_execute` returns "Python was not
     found" / errors with empty output (same Windows-Store-alias stub that makes a bridge fail to
     respawn). (AD >=1.9.63 PROVISIONS Python itself - a system install if one is on PATH, else a
     portable no-UAC python-build-standalone copy under `~/.adom/adom-runtimes/python-<ver>/`, pinned
     to 3.12.13 - and spawns `server.py` by ABSOLUTE PATH from it. So the bridge-SPAWN case is fully
     handled by AD; NEVER bootstrap/download your own Python. Check runtime state anytime with AD's
     `runtimes` verb (`state ∈ absent|installing|ready|failed`). But a bare `python` you invoke
     yourself via `shell_execute` still won't resolve, so the file-copy deploy below remains the way.)
     `cmd /c copy` and `robocopy` also fight you on the spaces in `Adom Desktop` / `Autodesk
     Fusion 360` (robocopy exit 16). **The reliable deploy that needs NO shell + NO python:** copy the
     changed add-in file(s) with the BRIDGE's own file verbs - `read_file {path:<cache>/addin/AdomBridge/
     commands/<f>.py}` then `write_file {path:<Roaming>/.../AddIns/AdomBridge/commands/<f>.py, content}`
     (Fusion CLOSED so the file is not locked). Verify with another `read_file` (grep a marker from your
     change). The Roaming AddIns path is `%APPDATA%/Roaming/Autodesk/Autodesk Fusion 360/API/AddIns/AdomBridge`.
4. **Verify by SHA256, never findstr.** `(Get-FileHash <Roaming>\commands\cloud_documents.py).Hash`
   must equal `sha256sum` of that same file unzipped from the published `v<ver>.zip`. findstr gives
   FALSE NEGATIVES on these files (UTF-8 box-drawing / dash chars break it) - it told me the reprimand
   was missing when the file was byte-identical to the release. Do not trust findstr; hash or
   `Get-Content -Raw` regex only.
5. **`fusion_start`** - Fusion now loads the new add-in. Confirm a `fusion_*` verb behaves new.

Memorize:
- `bridge_install` bumping `BRIDGE_VERSION` to the new version does NOT mean the running add-in changed.
  The version marker moves independently of the Roaming sync. Always hash-verify (step 5).
- The add-in sync only takes effect with **Fusion closed** (step 3). Skipping that is the #1 way a
  "successful" install leaves the OLD add-in running.
- **Never hand-edit the cache or the Roaming copy.** They get clobbered on the next stream/sync. Edit
  THIS source, publish, then run the sequence above.

## The publish/reinstall loop

See `skills/fusion-bridge-dev/SKILL.md` for the full ritual. Short form: edit here → bump
`BRIDGE_VERSION` + `bridge.json` together → `git commit` → **push GitHub backup** + **publish to the
wiki** (`wiki.adom.inc/adom/adom-desktop-fusion-bridge`, the canonical repo) → `bridge_install` on the
box → reap the old bridge instance + restart so the new code (and synced add-in) loads → anon-verify.
Wiki = canonical, GitHub = backup, keep in lockstep.

## Fusion electronics file hierarchy (the other thing you kept getting wrong)

A Fusion electronics design is a **parent/child chain of separate files**, each a distinct
`productType`. **ALWAYS open the PROJECT; never the schematic/.brd/3D directly.**

```
PROJECT        EcadDesignProductType    <- open THIS
  |- schematic   SchematicProductType
  |- board/.brd  BoardProductType
  |- 3D PCB      DesignProductType       (generated FROM the .brd - the LEAF; no editable board)
```

Opening a child gives an isolated/empty view (you can't pull a board out of the 3D). Same-named
copies in OTHER projects are usually 3D-model derivatives (`DesignProductType`), not the project.
The open verbs (`handle_open_cloud_file` / `handle_open_by_urn`) reprimand via `_hint` when you open
a non-`EcadDesignProductType` file - read it.
