skill
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.
← Commit history
standard 0.15.1: engine 0.12.1, plan_route explains no-path; fusion 1.13.9
8 files changed
+31−8
README.md+2@@ -158,6 +158,8 @@ Decisions so far (proposals from the KiCad and Fusion bridges, 2026-10-27; issue ## Status +Version 0.15.1: the engine (0.12.1)'s `plan_route` explains a no-path answer: its planner block, and the avoided pours at each end+pad with whether they are exempt (fusion-lane #21). Version 0.15.0: the engine (0.12.0) adds `assembly_check` (matching the Altium bridge's answers on its test boards) and `net_lengths`, both from the Altium bridge. Altium 0.2.80 answers the four planners from the engine. Version 0.14.0:
engine/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-eda-checks"-version = "0.12.0"+version = "0.12.1" dependencies = [ "roxmltree", "serde_json",
engine/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.12.0"+version = "0.12.1" edition = "2021" description = "The EDA Bridge Standard's checks and board-file writers, computed from the board model JSON: no bridge calls, no tool." license = "MIT"
engine/README.md+2−2@@ -24,7 +24,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. | `net_lengths` | board | the routed length of each net (tracks and arcs, by layer, with its vias), the skew of each differential pair (`pairs`, else found by net name: `_P`/`_N`, `+`/`-`) and the spread of each matched-length group against its tolerance (`groups`). From the Altium bridge's lengths.rs | | `placement_check` | board, bodies? | courtyard overlaps (pads and bodies, grown by `marginMm`), parts off or over the edge, pads within `edgeMm` of the edge, parts on mounting holes; `stacked` lists plan-view overlaps where one part sits wholly above the other (a part under a raised molecule: pads apart, no bodies meeting in 3D); `bodiesUsed: false` when no bodies were given | | `plan_fanout` | board, stack?, rules? | the fanout of a BGA or QFN/QFP (`ref`): pitch and package from its pads; a BGA's outer ring escapes on the surface and each inner ball takes a dogbone via, or a via in the pad when the dogbone does not fit (`style` auto); a perimeter pad takes an outward stub, with a via at its end for ground or `viaPerimeter`, and an exposed pad a via grid. `calls` are route_net arguments, one per net; `conflicts` lists each via and stub closer than the clearance to another net's pad, track, arc or via anywhere on the board. A pin its net's copper already reaches is skipped (`includeRouted` plans it anyway), the exposed-pad grid keeps clear of holes already there, and `crossesPours` names other nets' pours the plan runs into (repour before DRC). From the KiCad bridge's place_tools |-| `plan_route` | board, rules? | a path between two pads of a net (`from`, `to` as Ref.Pad) on any listed copper `layers`, with through vias between them: a bounded orthogonal A* on `gridMm`, width and via from the args, else the board's width and via rules (`planner.widthFrom`, `viaFrom`), keeping `widthMm/2` + the board's strictest board-wide copper clearance rule from other nets' copper (pads by their real outline), inside the outline, no via in a pad; a through via is checked on every copper layer of the board (`planner.viasCheckedOn`), not only the planned ones. `routeNet` is route_net's arguments, unchanged. Pours are not obstacles (repour after), but `crossesPours` names each other net's pour the route runs through, since that can split it; `avoidPours: true` keeps the route out of them (or a list of nets: out of theirs only), and `pourCost` makes a mm inside one cost 1 + pourCost mm, a preference where a hard block finds no path; a pour that holds the route's own end pad is exempt and named (`planner.avoidExempt`). The grid path is smoothed to straight and 45-degree segments where they keep the same clearance (`smooth: false` keeps it) |+| `plan_route` | board, rules? | a path between two pads of a net (`from`, `to` as Ref.Pad) on any listed copper `layers`, with through vias between them: a bounded orthogonal A* on `gridMm`, width and via from the args, else the board's width and via rules (`planner.widthFrom`, `viaFrom`), keeping `widthMm/2` + the board's strictest board-wide copper clearance rule from other nets' copper (pads by their real outline), inside the outline, no via in a pad; a through via is checked on every copper layer of the board (`planner.viasCheckedOn`), not only the planned ones. `routeNet` is route_net's arguments, unchanged. Pours are not obstacles (repour after), but `crossesPours` names each other net's pour the route runs through, since that can split it; `avoidPours: true` keeps the route out of them (or a list of nets: out of theirs only), and `pourCost` makes a mm inside one cost 1 + pourCost mm, a preference where a hard block finds no path; a pour that holds the route's own end pad is exempt and named (`planner.avoidExempt`); with no path, `ends` names the avoided pours at each end pad and whether they are exempt. The grid path is smoothed to straight and 45-degree segments where they keep the same clearance (`smooth: false` keeps it) | | `thermal_vias` | board, rules? | a grid of vias in a part's exposed pad (`ref`, `pad?`), clear of vias already there; `apply` is `place_vias` arguments | | `via_fence` | board, rules? | vias of `viaNet` both sides of the named nets' tracks and/or round the board edge (`edge`), at `pitchMm` or lambda/20 at `freqGHz`, clear of other copper; `apply` is `place_vias` arguments | @@ -98,7 +98,7 @@ adom-eda-checks import_svg --svg logo.svg --args '{"at": [10, 10], "widthMm": 20 Replies keep the standard envelope (`success`, `errorCode`, `error`, `_hint`) and name verbs without a prefix (`apply_pours`, `add_rule`, `place_vias`): a bridge adds its own. Every reply also carries `engine`-(`"adom-eda-checks 0.12.0"`), so a bridge reports the engine it runs without writing the version by hand.+(`"adom-eda-checks 0.12.1"`), so a bridge reports the engine it runs without writing the version by hand. ## Tests
engine/src/route.rs+22−1@@ -280,7 +280,20 @@ pub fn plan_route(input: &Value) -> Value { let revision = st["revision"].clone(); let Some(goal_node) = found else { let why = if seen.len() >= max_nodes { "no path within the search limit (maxNodes): try a coarser gridMm or a higher maxNodes" } else { "no legal path on this grid: other copper, the outline or the clearance blocks every way. A fine-pitch pad (0.5 mm pitch) usually needs gridMm 0.1 to escape; more layers give more room" };- return json!({"success": true, "found": false, "reason": why, "expandedNodes": seen.len(), "revision": revision});+ // what an avoided pour does at each end: a pour the end pad sits in is exempt; one that only touches the pad's+ // first step out still walls it in (fusion-lane #21: avoidPours ["VBAT"] found no path from SW1.2)+ let near_ends: Vec<Value> = ends.iter().map(|p| {+ let c = (f(&p["x"]), f(&p["y"]));+ let reach = grid + width / 2.0 + clearance;+ let touching: Vec<Value> = (0..pours.len()).filter(|&i| avoids(&pours[i].0) && pad_layers(p, &copper).contains(&pours[i].1) && pours[i].2.dist(c, c) <= reach)+ .map(|i| json!({"net": pours[i].0, "layer": pours[i].1, "exempt": held.contains(&i)})).collect();+ json!({"pad": key(p), "avoidedPoursHere": touching})+ }).collect();+ let walled = near_ends.iter().any(|e| e["avoidedPoursHere"].as_array().map(|a| a.iter().any(|x| x["exempt"] == json!(false))).unwrap_or(false));+ let why = if avoid_nets.is_some() && walled { "no legal path: an avoided pour touches an end pad without holding it (avoidedPoursHere), so the route cannot leave the pad without entering it. Try pourCost instead of avoidPours, or drop that net from avoidPours" } else { why };+ return json!({"success": true, "found": false, "reason": why, "expandedNodes": seen.len(), "revision": revision, "ends": near_ends,+ "planner": {"algorithm": "bounded orthogonal A*", "widthFrom": width_from, "viaFrom": via_from, "gridMm": grid, "layers": layers, "viasCheckedOn": copper, "clearanceMm": clearance, "edgeClearanceMm": edge,+ "avoidPours": args["avoidPours"].clone(), "avoidExempt": exempt, "pourCost": pour_cost, "expandedNodes": seen.len()}}); }; let mut path = vec![goal_node]; while let Some(p) = parent.get(path.last().unwrap()) { path.push(*p); }@@ -466,6 +479,14 @@ mod tests { assert_eq!(held["found"], json!(true), "{held}"); assert_eq!(held["planner"]["avoidExempt"][0]["net"], "GND"); assert_eq!(held["planner"]["pourCost"], json!(0.0));+ // a pour that touches U1.1 without holding it walls the pad in: the no-path reply says so, with its planner block+ let mut touch = board(json!([]));+ touch["polygons"] = json!([{"net": "GND", "layer": "F.Cu", "points": [[2.6, 0.0], [12.0, 0.0], [12.0, 10.0], [2.6, 10.0]]}, {"net": "GND", "layer": "B.Cu", "points": [[2.6, 0.0], [12.0, 0.0], [12.0, 10.0], [2.6, 10.0]]}]);+ let walled = plan_route(&json!({"board": touch, "args": {"net": "SIG", "from": "U1.1", "to": "J1.1", "avoidPours": ["GND"]}}));+ assert_eq!(walled["found"], json!(false), "{walled}");+ assert_eq!(walled["ends"][0]["avoidedPoursHere"][0], json!({"net": "GND", "layer": "F.Cu", "exempt": false}), "{walled}");+ assert!(walled["reason"].as_str().unwrap().contains("avoidedPoursHere"));+ assert_eq!(walled["planner"]["avoidPours"], json!(["GND"])); // and with the cost a short way round a narrow pour beats crossing it let mut strip = board(json!([])); strip["polygons"] = json!([{"net": "GND", "layer": "F.Cu", "points": [[9.0, 2.0], [11.0, 2.0], [11.0, 10.0], [9.0, 10.0]]}]);
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "eda-bridge-standard",- "version": "0.15.0",+ "version": "0.15.1", "type": "skill", "title": "EDA Bridge Standard: one way for an AI to drive every PCB tool", "description": "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.",
verbs/coverage.json+1−1@@ -1,5 +1,5 @@ {- "standard": "0.15.0",+ "standard": "0.15.1", "reference": { "bridge": "altium", "version": "0.2.80"
verbs/standard.json+1−1@@ -1,5 +1,5 @@ {- "standard": "0.15.0",+ "standard": "0.15.1", "reference": { "bridge": "altium", "version": "0.2.80"