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.
main
Colby Knox
standard 0.4.0: the checks engine adom-eda-checks in engine/ (Rust crate + CLI, JSON in, no bridge calls): copper_balan…
c380046
14d ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
//! The input bundle every check takes: `{board, stack?, pours?, rules?, silk?, fab?, bodies?, sheetParts?, netlist?, args?}`, each the
//! standard model a bridge answers (schema/board.schema.json, schema/companions.schema.json). A bridge's whole reply
//! is accepted too: the model's keys at the top level, or under `data` (the reply envelope keys are ignored).
use serde_json::{json, Value};
/// A refusal in the standard reply envelope.
pub fn refuse(code: &str, error: &str) -> Value {
json!({"success": false, "errorCode": code, "error": error})
}
/// The companion under `key`, unwrapped from a reply's `data` when it came as one.
pub fn part(input: &Value, key: &str) -> Option<Value> {
let v = input.get(key)?;
if v.is_null() { return None; }
Some(if v.get("data").map(|d| d.is_object()).unwrap_or(false) { v["data"].clone() } else { v.clone() })
}
/// The board model, or `bad_args` when there is none or the bridge's reply was a refusal.
pub fn board(input: &Value) -> Result<Value, Value> {
let Some(b) = part(input, "board") else { return Err(refuse("bad_args", "the input has no board: pass the board model (routing_state) as board")) };
if b.get("success") == Some(&json!(false)) { return Err(b); }
Ok(b)
}
/// The stack-up companion (layer_stack), if given.
pub fn stack(input: &Value) -> Option<Value> { part(input, "stack") }
/// The poured copper (pour_state's `zones`), empty when not given.
pub fn pours(input: &Value) -> Vec<Value> {
let Some(p) = part(input, "pours") else { return Vec::new() };
p.get("zones").or(Some(&p)).and_then(|z| z.as_array()).cloned().unwrap_or_default()
}
/// The board's rules (read_rules): a list, or a reply with `rules`.
pub fn rules(input: &Value) -> Vec<Value> {
let Some(r) = part(input, "rules") else { return Vec::new() };
r.get("rules").or(Some(&r)).and_then(|x| x.as_array()).cloned().unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn companions_come_bare_or_in_a_reply() {
let bare = json!({"board": {"outline": []}, "stack": {"layers": [{"name": "Top"}]}, "pours": {"zones": [{"net": "GND"}]}});
let wrapped = json!({"board": {"success": true, "outline": []}, "stack": {"success": true, "data": {"layers": [{"name": "Top"}]}}, "pours": [{"net": "GND"}]});
for i in [bare, wrapped] {
assert!(board(&i).is_ok());
assert_eq!(stack(&i).unwrap()["layers"][0]["name"], "Top");
assert_eq!(pours(&i)[0]["net"], "GND");
}
assert_eq!(board(&json!({})).unwrap_err()["errorCode"], "bad_args");
}
}