EDA Bridge Standard: one way for an AI to drive every PCB tool
Public Made by Adomby adom
The standard every Adom EDA bridge follows so one AI can drive Altium, KiCad, Fusion 360 and any future PCB tool the same way: shared verb names in tiers, the board model JSON, reply and refusal conventions, and a coverage table of each bridge against the Altium reference.
Conventions every bridge follows
These started as the rules the reference bridge (Altium) keeps, and grow with what every bridge learns. A bridge that keeps them lets one AI drive every EDA tool the same way, and lets the shared checks run on any of them.
Names
- A verb is
<prefix>_<name>:altium_route_net,kicad_route_net,fusion_route_net. The<name>comes fromverbs/standard.json. A tool that does the same job under its own verb keeps it and adds the standard name as an alias, sodocs/coverage.mdcan list it as exact. - A verb a tool cannot do at all is not faked. Leave it out, or answer
success:falsewitherrorCode: "not_supported"and anerrorthat says why. - Coverage counts only what works. A verb that answers
not_supportedfor its whole job is markedunsupportedin the bridge'stools/equivalents/<bridge>.jsonwith the reason (["sch_update_from_library", "unsupported", "the tool's library manager asks about each part"]): listed, not counted. A verb that refuses some of its arguments ispartial, with a note naming them. The entry wins over a name match, so this works for a verb under its standard name too. - Tool-only verbs (Altium 365, OutJobs) keep their own names under the
platformtier and stay out of the standard. - A job the standard lacks is proposed in
tools/proposed/<thread>.jsonand joins once another bridge's thread agrees (README, Proposing a verb). Board verbs have no prefix and call a placed part a part; sheet verbs start withsch_;*_symboland*_footprintact on library files.
Units, layers, frames
- Millimetres everywhere in replies and arguments; degrees for angles, counterclockwise from +x. An argument in other
units says so in its name (
copperUm,wMil). - Coordinates are absolute in the standard frame: the tool's origin and x axis, y up (a y-down tool negates y and
nothing else). What
routing_statereports is whatroute_nettakes. See board-model.md. - A reply field that carries its unit in its name says mm:
xMm,widthMm,areaMm2. Some of the reference bridge's older verbs still answer in mils (xMil, markedunitsNoteinverbs/standard.json); that is the reference's debt, not the standard. A new bridge answers those verbs in mm underMmnames. - Schematic sheets follow the same rule: millimetres, the tool's sheet origin, y up. A pin is named
Ref.Pin(U1.3), and an item (a wire, a label, a junction) by the tool's own stableid, assch_itemslists it. sheetis the sheet's file path. On a tool whose sheets are not files (Fusion keeps them in one design),sheetis the sheet's number, from 1, andfilePathnames the design. A tool that stores no item ids builds stable ones (Fusion: the item's kind and position) and says so insch_items' hint.savedefaults to true on the sheet verbs, except where a save has a cost the user would notice (Fusion: every save is a new cloud version): there it defaults to false, and the verb's describe says so.- Copper layers carry KiCad's names on every tool:
F.Cu,In1.CutoIn30.Cu,B.Cu, and*.Cufor a pad on every copper layer. A layer argument also takes the tool's own id. - Nets are named as the design names them; matching is case-insensitive.
Replies
Every reply is one JSON object:
| Key | Meaning |
|---|---|
success |
true when the verb did what it says. A check that ran and found problems is success: true with its findings; ok (or fits, ready) carries the verdict |
errorCode |
on success:false: a short stable code from the table below |
error |
on success:false: one sentence a person can read, naming the thing (the net, the pad, the file) |
_hint |
what to do next, in words. The AI reads replies, not skill files: the hint is where the next step is taught |
related / _next |
verbs that usually follow |
revision |
after an edit: the board's new revision (pass it back as expectedRevision) |
bridgeVersion |
the bridge's version |
windowEtiquette |
background when the verb took no focus from the user |
Verbs that change a design:
- Take
expectedRevisionand refuse withstale_boardwhen the board changed since the caller read it. - Land as one undo step in the tool.
- Read back what they wrote and report it: positions to 0.001 mm, rule descriptors from the tool, poured area.
- Plan before acting when the plan can be wrong:
dryRun: trueanswers the plan (points, sizes, what it replaces) and changes nothing. Planners that can short something check the plan first and refuse with the problem and the planned geometry (pair_plan_blocked), never place it. - Never save over a file without being told (
overwrite: true), and never answer a save prompt for the user.
Checks:
- Name each finding with where it is (
at: [x, y], a designator, a padU1.3, a net). - Separate errors from warnings, and say when a check could not see something (
noBody,poursWithoutRegions,complete: false) rather than passing it. - Read a tool's numeric constants from the tool, never assume them. A severity (error or warning), a state (approved,
waived) or a kind that the tool hands over as a number means what the tool's own API defines, and that differs from
tool to tool: Fusion's ULP has
ERROR_TYPE_WARNING = 1,ERROR_TYPE_ERROR = 2andERROR_STATE_APPROVED = 2, and the Fusion bridge reported DRC errors as warnings and warnings as errors until it read them. Answer the standard's words (severity: "error"), not the tool's numbers. - Name the source of any outside number (a fab's limit: its page and the date it was read). Fab limits come from adom/pcb-design-rules, the canonical record per vendor.
Refusal codes
The common ones. A bridge adds its own for its tool's states, in the same snake_case form.
| Code | Meaning |
|---|---|
bad_args |
a required argument is missing or out of range: error says which |
not_supported |
this tool cannot do this verb |
app_not_running / <tool>_not_running |
start the tool first (launch) |
not_licensed |
the tool runs without a licence that allows scripting |
no_current_board / no_board |
no board document is open: open one or pass filePath |
wrong_board / board_mismatch |
the open board is not the one named |
stale_board |
expectedRevision no longer matches: read the board again |
no_such_net / no_such_pad / part_not_found / designator_not_found |
the named thing is not on the board |
no_outline |
the board has no outline |
not_routed |
the net has no routed copper |
blocked_by_dialog |
a dialog in the tool blocks the verb: dialogs names it |
project_not_found / not_a_schdoc / unsupported_document |
the file is not what the verb needs |
write_failed |
the bridge could not write a file it was asked to |
nothing_to_do |
the request asked for no change |
*_plan_blocked, no_room, outline_on_mechanical_layer |
a planner refusing a plan that would be wrong, with why |
Driving the user's tool
- Background first: verbs work without taking the window or the mouse. A verb that must take focus says so in its docs and gives the user's window back after.
- A dialog the user would have to dismiss is a bridge defect: pre-empt it, answer it only when the answer carries no decision (a lone OK), keep a picture of anything answered without being read, and never answer a save prompt.
- Never kill the tool when it can be closed: a killed EDA tool can strand a licence seat.
Every verb documents itself
<prefix>_describe returns every verb with summary, input, output, hint, pitfalls, related, example and
timeoutSeconds. verbs/standard.json is generated from the reference bridge's describe and the agreed proposals in
tools/proposed/.
Shared vocabularies
Some verbs answer with a fixed set of words, so an AI reads every bridge's answer the same way. A bridge can add a word by proposing it on the page's issues.
update_footprints_from_library statuses (#27)
| status | meaning |
|---|---|
up_to_date |
the placed footprint matches the library's |
differs |
it doesn't; diff says how (padsAdded, padsRemoved, padsChanged[{pad, changes}], bodies3d?) |
library_not_found |
the footprint's library or the footprint in it wasn't found; why says which |
unusable |
the library copy was found but can't be laid onto the board; why says why. Read it like library_not_found |
A plan (apply false, the default) changes nothing. Tracks don't follow pads that moved.
lint_library rules (#28)
Severity belongs to the rule, not the bridge. A bridge emits only the rules it can evaluate, so a missing finding is not a pass. ok is true when there are no errors and nothing is unread; warnings don't fail it. The shared checks engine (lint_library) evaluates every rule but tool_refuses_file from the footprints and symbols a bridge reads.
| kind | rule | severity | what it means |
|---|---|---|---|
| footprint | no_pads |
error | no pads at all |
| footprint | zero_size_pad |
error | a pad with no copper and no hole |
| footprint | unnumbered_pad |
warning | a pad that isn't mechanical has no number |
| footprint | duplicate_pad_number |
warning | one number on lands apart from each other (pads that touch or overlap are one land) |
| footprint | no_courtyard |
warning | no courtyard |
| footprint | origin_off_part |
warning | the origin is outside the pads and courtyard, grown by 1 mm |
| footprint | no_silkscreen |
info | no silkscreen |
| footprint | no_3d_body |
info | no 3D body |
| footprint | no_description |
info | no description |
| symbol | no_pins |
error | no pins |
| symbol | footprint_not_found |
error | the linked footprint isn't in the libraries checked |
| symbol | pin_without_pad |
error | a pin number has no pad on the linked footprint |
| symbol | unnumbered_pin |
warning | a pin has no number |
| symbol | duplicate_pin_number |
warning | two pins share a number |
| symbol | no_footprint |
warning | no footprint is linked |
| symbol | pad_without_pin |
warning | a pad (not mechanical) on the linked footprint has no pin |
| symbol | no_part_number |
info | no manufacturer part number |
| library | tool_refuses_file |
error | the tool itself won't read the file; detail carries its message |
sheetDrawing (the drawing companion for schematic_review, #29)
One entry per sheet, in mm with y down (a tool that keeps mils converts):
{"sheet": "/Power/", "wires": [{"a": [101.6, 76.2], "b": [127.0, 76.2]}], "buses": [{"a": [50.8, 50.8], "b": [50.8, 101.6]}],
"junctions": [[127.0, 76.2]], "noConnects": [[152.4, 88.9]],
"labels": [{"text": "SDA", "kind": "local | global | hierarchical | power", "at": [120.65, 63.5]}],
"pins": [{"ref": "U1", "number": "3", "at": [101.6, 75.89]}], "symbols": [{"ref": "U1", "box": [80.0, 60.0, 101.6, 95.0]}]}
A point connects when it lies within 0.01 mm of another item. A wire end on another wire's middle connects only where there is a junction. A symbol's box is its body and pins, not its fields. An entry may carry sizeMm: [width, height], the sheet's size, so y counts down from its top edge (altium-lane). Without this companion, dangling_wire_end, floating_label and overlap are listed in unread.
# Conventions every bridge follows
These started as the rules the reference bridge (Altium) keeps, and grow with what every bridge learns. A bridge that keeps them lets one AI drive every EDA tool
the same way, and lets the shared checks run on any of them.
## Names
- A verb is `<prefix>_<name>`: `altium_route_net`, `kicad_route_net`, `fusion_route_net`. The `<name>` comes from
`verbs/standard.json`. A tool that does the same job under its own verb keeps it and adds the standard name as an
alias, so `docs/coverage.md` can list it as exact.
- A verb a tool cannot do at all is not faked. Leave it out, or answer `success:false` with
`errorCode: "not_supported"` and an `error` that says why.
- Coverage counts only what works. A verb that answers `not_supported` for its whole job is marked `unsupported` in the
bridge's `tools/equivalents/<bridge>.json` with the reason (`["sch_update_from_library", "unsupported", "the tool's
library manager asks about each part"]`): listed, not counted. A verb that refuses some of its arguments is `partial`,
with a note naming them. The entry wins over a name match, so this works for a verb under its standard name too.
- Tool-only verbs (Altium 365, OutJobs) keep their own names under the `platform` tier and stay out of the standard.
- A job the standard lacks is proposed in `tools/proposed/<thread>.json` and joins once another bridge's thread agrees (README,
*Proposing a verb*). Board verbs have no prefix and call a placed part a part; sheet verbs start with `sch_`;
`*_symbol` and `*_footprint` act on library files.
## Units, layers, frames
- Millimetres everywhere in replies and arguments; degrees for angles, counterclockwise from +x. An argument in other
units says so in its name (`copperUm`, `wMil`).
- Coordinates are absolute in the standard frame: the tool's origin and x axis, y up (a y-down tool negates y and
nothing else). What `routing_state` reports is what `route_net` takes. See [board-model.md](board-model.md).
- A reply field that carries its unit in its name says mm: `xMm`, `widthMm`, `areaMm2`. Some of the reference bridge's
older verbs still answer in mils (`xMil`, marked `unitsNote` in `verbs/standard.json`); that is the reference's debt,
not the standard. A new bridge answers those verbs in mm under `Mm` names.
- Schematic sheets follow the same rule: millimetres, the tool's sheet origin, y up. A pin is named `Ref.Pin`
(`U1.3`), and an item (a wire, a label, a junction) by the tool's own stable `id`, as `sch_items` lists it.
- `sheet` is the sheet's file path. On a tool whose sheets are not files (Fusion keeps them in one design), `sheet` is
the sheet's number, from 1, and `filePath` names the design. A tool that stores no item ids builds stable ones (Fusion:
the item's kind and position) and says so in `sch_items`' hint.
- `save` defaults to true on the sheet verbs, except where a save has a cost the user would notice (Fusion: every save
is a new cloud version): there it defaults to false, and the verb's describe says so.
- Copper layers carry KiCad's names on every tool: `F.Cu`, `In1.Cu` to `In30.Cu`, `B.Cu`, and `*.Cu` for a pad on every
copper layer. A layer argument also takes the tool's own id.
- Nets are named as the design names them; matching is case-insensitive.
## Replies
Every reply is one JSON object:
| Key | Meaning |
|---|---|
| `success` | true when the verb did what it says. A check that ran and found problems is `success: true` with its findings; `ok` (or `fits`, `ready`) carries the verdict |
| `errorCode` | on `success:false`: a short stable code from the table below |
| `error` | on `success:false`: one sentence a person can read, naming the thing (the net, the pad, the file) |
| `_hint` | what to do next, in words. The AI reads replies, not skill files: the hint is where the next step is taught |
| `related` / `_next` | verbs that usually follow |
| `revision` | after an edit: the board's new revision (pass it back as `expectedRevision`) |
| `bridgeVersion` | the bridge's version |
| `windowEtiquette` | `background` when the verb took no focus from the user |
Verbs that change a design:
- Take `expectedRevision` and refuse with `stale_board` when the board changed since the caller read it.
- Land as one undo step in the tool.
- Read back what they wrote and report it: positions to 0.001 mm, rule descriptors from the tool, poured area.
- Plan before acting when the plan can be wrong: `dryRun: true` answers the plan (points, sizes, what it replaces) and
changes nothing. Planners that can short something check the plan first and refuse with the problem and the planned
geometry (`pair_plan_blocked`), never place it.
- Never save over a file without being told (`overwrite: true`), and never answer a save prompt for the user.
Checks:
- Name each finding with where it is (`at: [x, y]`, a designator, a pad `U1.3`, a net).
- Separate errors from warnings, and say when a check could not see something (`noBody`, `poursWithoutRegions`,
`complete: false`) rather than passing it.
- Read a tool's numeric constants from the tool, never assume them. A severity (error or warning), a state (approved,
waived) or a kind that the tool hands over as a number means what the tool's own API defines, and that differs from
tool to tool: Fusion's ULP has `ERROR_TYPE_WARNING = 1`, `ERROR_TYPE_ERROR = 2` and `ERROR_STATE_APPROVED = 2`, and
the Fusion bridge reported DRC errors as warnings and warnings as errors until it read them. Answer the standard's
words (`severity: "error"`), not the tool's numbers.
- Name the source of any outside number (a fab's limit: its page and the date it was read). Fab limits come from
[adom/pcb-design-rules](https://wiki.adom.inc/adom/pcb-design-rules), the canonical record per vendor.
## Refusal codes
The common ones. A bridge adds its own for its tool's states, in the same snake_case form.
| Code | Meaning |
|---|---|
| `bad_args` | a required argument is missing or out of range: `error` says which |
| `not_supported` | this tool cannot do this verb |
| `app_not_running` / `<tool>_not_running` | start the tool first (`launch`) |
| `not_licensed` | the tool runs without a licence that allows scripting |
| `no_current_board` / `no_board` | no board document is open: open one or pass `filePath` |
| `wrong_board` / `board_mismatch` | the open board is not the one named |
| `stale_board` | `expectedRevision` no longer matches: read the board again |
| `no_such_net` / `no_such_pad` / `part_not_found` / `designator_not_found` | the named thing is not on the board |
| `no_outline` | the board has no outline |
| `not_routed` | the net has no routed copper |
| `blocked_by_dialog` | a dialog in the tool blocks the verb: `dialogs` names it |
| `project_not_found` / `not_a_schdoc` / `unsupported_document` | the file is not what the verb needs |
| `write_failed` | the bridge could not write a file it was asked to |
| `nothing_to_do` | the request asked for no change |
| `*_plan_blocked`, `no_room`, `outline_on_mechanical_layer` | a planner refusing a plan that would be wrong, with why |
## Driving the user's tool
- Background first: verbs work without taking the window or the mouse. A verb that must take focus says so in its
docs and gives the user's window back after.
- A dialog the user would have to dismiss is a bridge defect: pre-empt it, answer it only when the answer carries no
decision (a lone OK), keep a picture of anything answered without being read, and never answer a save prompt.
- Never kill the tool when it can be closed: a killed EDA tool can strand a licence seat.
## Every verb documents itself
`<prefix>_describe` returns every verb with `summary`, `input`, `output`, `hint`, `pitfalls`, `related`, `example` and
`timeoutSeconds`. `verbs/standard.json` is generated from the reference bridge's `describe` and the agreed proposals in
`tools/proposed/`.
## Shared vocabularies
Some verbs answer with a fixed set of words, so an AI reads every bridge's answer the same way. A bridge can add a word by proposing it on the page's issues.
### `update_footprints_from_library` statuses (#27)
| status | meaning |
|---|---|
| `up_to_date` | the placed footprint matches the library's |
| `differs` | it doesn't; `diff` says how (`padsAdded`, `padsRemoved`, `padsChanged[{pad, changes}]`, `bodies3d?`) |
| `library_not_found` | the footprint's library or the footprint in it wasn't found; `why` says which |
| `unusable` | the library copy was found but can't be laid onto the board; `why` says why. Read it like `library_not_found` |
A plan (`apply` false, the default) changes nothing. Tracks don't follow pads that moved.
### `lint_library` rules (#28)
Severity belongs to the rule, not the bridge. A bridge emits only the rules it can evaluate, so a missing finding is not a pass. `ok` is true when there are no errors and nothing is `unread`; warnings don't fail it. The shared checks engine (`lint_library`) evaluates every rule but `tool_refuses_file` from the footprints and symbols a bridge reads.
| kind | rule | severity | what it means |
|---|---|---|---|
| footprint | `no_pads` | error | no pads at all |
| footprint | `zero_size_pad` | error | a pad with no copper and no hole |
| footprint | `unnumbered_pad` | warning | a pad that isn't mechanical has no number |
| footprint | `duplicate_pad_number` | warning | one number on lands apart from each other (pads that touch or overlap are one land) |
| footprint | `no_courtyard` | warning | no courtyard |
| footprint | `origin_off_part` | warning | the origin is outside the pads and courtyard, grown by 1 mm |
| footprint | `no_silkscreen` | info | no silkscreen |
| footprint | `no_3d_body` | info | no 3D body |
| footprint | `no_description` | info | no description |
| symbol | `no_pins` | error | no pins |
| symbol | `footprint_not_found` | error | the linked footprint isn't in the libraries checked |
| symbol | `pin_without_pad` | error | a pin number has no pad on the linked footprint |
| symbol | `unnumbered_pin` | warning | a pin has no number |
| symbol | `duplicate_pin_number` | warning | two pins share a number |
| symbol | `no_footprint` | warning | no footprint is linked |
| symbol | `pad_without_pin` | warning | a pad (not mechanical) on the linked footprint has no pin |
| symbol | `no_part_number` | info | no manufacturer part number |
| library | `tool_refuses_file` | error | the tool itself won't read the file; `detail` carries its message |
### `sheetDrawing` (the drawing companion for `schematic_review`, #29)
One entry per sheet, in mm with y down (a tool that keeps mils converts):
```json
{"sheet": "/Power/", "wires": [{"a": [101.6, 76.2], "b": [127.0, 76.2]}], "buses": [{"a": [50.8, 50.8], "b": [50.8, 101.6]}],
"junctions": [[127.0, 76.2]], "noConnects": [[152.4, 88.9]],
"labels": [{"text": "SDA", "kind": "local | global | hierarchical | power", "at": [120.65, 63.5]}],
"pins": [{"ref": "U1", "number": "3", "at": [101.6, 75.89]}], "symbols": [{"ref": "U1", "box": [80.0, 60.0, 101.6, 95.0]}]}
```
A point connects when it lies within 0.01 mm of another item. A wire end on another wire's middle connects only where there is a junction. A symbol's `box` is its body and pins, not its fields. An entry may carry `sizeMm: [width, height]`, the sheet's size, so y counts down from its top edge (altium-lane). Without this companion, `dangling_wire_end`, `floating_label` and `overlap` are listed in `unread`.