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.28.0: engine 0.23.0 (sheetDrawing checks, flat test points)
12 files changed
+146−14
README.md+1@@ -158,6 +158,7 @@ Decisions so far (proposals from the KiCad and Fusion bridges, 2026-10-27; issue ## Status +Version 0.28.0: engine 0.23.0: `schematic_review` reads a sheet's drawing (`sheetDrawing`, kicad-lane's shape, #29) for dangling wire ends, floating labels and overlapping symbols; `placement_check` counts a body-less SMD test point as flat under a raised module. Version 0.27.2: coverage reads KiCad bridge 1.0.92 (log_tail exact; sch_batch partial). Version 0.27.1: coverage reads Fusion 360 bridge 1.13.30 (engine 0.22.0). Version 0.27.0: engine 0.22.0 ports kicad-bridge's design checks into `schematic_review` (I2C pull-ups, reset and enable pins, LED resistors, regulator and bulk capacitors, annotation, no-connect flags on wired pins, two names on one net) and its placement score into `placement_check` (0-100 and a grade; connectors far from the edge, decoupling far from its IC, ratsnest length) (#29). The reference is Altium bridge 0.2.85; coverage reads Fusion 360 bridge 1.13.29.
docs/conventions.md+13@@ -152,3 +152,16 @@ Severity belongs to the rule, not the bridge. A bridge emits only the rules it c | 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. Without this companion, `dangling_wire_end`, `floating_label` and `overlap` are listed in `unread`.
engine/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-eda-checks"-version = "0.22.0"+version = "0.23.0" dependencies = [ "roxmltree", "serde_json",
engine/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.22.0"+version = "0.23.0" 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@@ -31,12 +31,12 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. | `length_tune` | board, rules? | a serpentine on the net's longest straight track with room beside it, its bumps sized to add exactly what `targetMm` (or `matchTo`, another net's length) asks, every bump `clearance` (the board's strictest rule) from other nets' pads, routed tracks and vias and inside the board; answered as the track to `remove` and the `routeNet` that replaces it. From the Altium bridge's lengthtune.rs | | `lint_library` | library | is a symbol or footprint library fit to ship (standard #28): the shared rules with a fixed severity each (`no_pads`, `zero_size_pad`, `duplicate_pad_number`, `no_courtyard`, `origin_off_part`, `pin_without_pad`, `pad_without_pin`, `no_part_number`, ...; `rules` in the reply lists them), checked from the footprints and symbols the bridge sends; a key left out is not checked, a key sent empty is a finding; the bridge's own findings (`tool_refuses_file`) pass through | | `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; plus a 0-100 score and grade (from kicad-bridge's score_placement): connectors farther than `connectorEdgeMm` from the edge, decoupling capacitors farther than `decouplingMm` from their IC's pin on the same rail, and the ratsnest's length against the board diagonal |+| `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; plus a 0-100 score and grade (from kicad-bridge's score_placement): connectors farther than `connectorEdgeMm` from the edge, decoupling capacitors farther than `decouplingMm` from their IC's pin on the same rail, and the ratsnest's length against the board diagonal; a body-less SMD test point counts as flat, so it can sit stacked under a raised module | | `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`); with no path, `ends` names the avoided pours at each end pad and whether they are exempt, and `withoutAvoid` says whether a path exists through them. The grid path is smoothed to straight and 45-degree segments where they keep the same clearance (`smooth: false` keeps it) | | `review_board` | board, sheetParts?, rules? | small design-review checks, errors first: placeholder values, single-pin nets, duplicate designators, parts with pads off the outline polygon or near the edge (`edgeMm`), parts with no footprint name, off-grid parts (with `gridMm`); from the sheet parts, missing Mouser/DigiKey/LCSC numbers and datasheets and placeholder comments; from the rules, disabled ones, missing clearance/width/hole-size/short-circuit rules and same-priority ties (rules classed from a standard kind, a descriptor, Altium's ordinal or the rule's own name; `alwaysChecked` names classes a tool always checks, like EAGLE's shorts). `skipped` and `notImplemented` say what was not judged. From the Altium bridge's review.rs | | `route_pair` | board, stack?, rules? | a differential pair planned as one centreline (pad midpoints through `via` waypoints) with each net offset half a pitch to its own side, mitred corners and a `fanMm` fan-out; width and gap given, or from `diffOhms` and the stack. `problems` names a crossing or other copper (pads by outline, routed tracks, vias) closer than the clearance; `routeNet` holds both nets' route_net arguments and `widthRule` the pair's add_rule. From the Altium bridge's pairroute.rs |-| `schematic_review` | sheetParts, netlist | every placed pin, with its electrical type, against the compiled netlist: power pins on no net and outputs driving each other (errors); unconnected pins, one-pin nets, inputs nothing drives and IC supplies with no capacitor to ground (warnings; a regulator's switch node on an inductor is no missing capacitor). From the Altium bridge's schreview.rs; plus the design checks from kicad-bridge's design_review and sch_check: I2C lines with no pull-up, reset and enable pins floating or with nothing setting their level, LEDs with no series resistor, regulators missing input or output capacitors, rails with no bulk capacitor, unannotated or doubled designators, no-connect flags on wired pins, and two names on one net (`netlist.names`); each with `message` and `fix` |+| `schematic_review` | sheetParts, netlist | every placed pin, with its electrical type, against the compiled netlist: power pins on no net and outputs driving each other (errors); unconnected pins, one-pin nets, inputs nothing drives and IC supplies with no capacitor to ground (warnings; a regulator's switch node on an inductor is no missing capacitor). From the Altium bridge's schreview.rs; plus the design checks from kicad-bridge's design_review and sch_check: I2C lines with no pull-up, reset and enable pins floating or with nothing setting their level, LEDs with no series resistor, regulators missing input or output capacitors, rails with no bulk capacitor, unannotated or doubled designators, no-connect flags on wired pins, and two names on one net (`netlist.names`); each with `message` and `fix`; with the `sheetDrawing` companion (one entry per sheet: wires, buses, junctions, noConnects, labels, pins, symbol boxes, mm, y down) also dangling wire ends (with the nearest pin or label), labels on no wire or pin, and symbols drawn over each other, which are listed in `unread` without it | | `silk_designators` | board, silk | a place for each reference designator: one size (`size`, stepping down to `minSize`), beside its own part, clear of every pad by `padClearance`, of other parts' bodies and of other labels, inside the board. `applyArgs` is apply_silkscreen's arguments; parts with no clear spot are listed, not forced. From the Altium bridge's silkplace.rs | | `teardrops` | board, rules? | a tapered fill where a track meets a via or pad of its own net (`targets` via, tht, smd), as wide as the pad at its centre (`widthRatio`) and narrowing to the track `lengthRatio` x the pad's size past its edge; each a polygon on the track's layer kept clear of other nets' copper by the board's clearance, the joints with no room skipped and named | | `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 |
engine/src/lib.rs+1−1@@ -4,7 +4,7 @@ //! answers the verb under its own prefix (`kicad_return_path`). Replies keep the standard envelope (`success`, //! `errorCode`, `error`, `_hint`) and name verbs without a prefix. //!-//! Every check takes one input bundle, `{board, stack?, pours?, rules?, silk?, fab?, bodies?, prices?, sheetParts?, netlist?, library?, args?}` (see `input`).+//! Every check takes one input bundle, `{board, stack?, pours?, rules?, silk?, fab?, bodies?, prices?, sheetParts?, netlist?, sheetDrawing?, library?, args?}` (see `input`). use serde_json::Value; pub mod assembly;
engine/src/main.rs+2−2@@ -23,8 +23,8 @@ fn bundle(rest: &[String]) -> Result<Value, String> { while let Some(flag) = it.next() { let val = it.next().ok_or_else(|| format!("{flag} needs a value"))?; match flag.as_str() {- "--board" | "--stack" | "--pours" | "--rules" | "--silk" | "--fab" | "--bodies" | "--prices" | "--sheet-parts" | "--netlist" | "--drc" | "--library" => {- let key = match flag.as_str() { "--sheet-parts" => "sheetParts", f => &f[2..] };+ "--board" | "--stack" | "--pours" | "--rules" | "--silk" | "--fab" | "--bodies" | "--prices" | "--sheet-parts" | "--netlist" | "--drc" | "--library" | "--sheet-drawing" => {+ let key = match flag.as_str() { "--sheet-parts" => "sheetParts", "--sheet-drawing" => "sheetDrawing", f => &f[2..] }; b[key] = read(val)?; } "--args" => b["args"] = serde_json::from_str(val).map_err(|e| format!("--args: {e}"))?,
engine/src/placement.rs+19−2@@ -111,10 +111,20 @@ pub fn is_stacked(pads: &[Value], bs: &[crate::bodies::BodyBox], a: &str, b: &st }).collect() }; let (pa, pb) = (pad_rects(a), pad_rects(b)); if pa.is_empty() || pb.is_empty() || pa.iter().any(|x| pb.iter().any(|y| hit(grow(*x, margin), grow(*y, margin)))) { return false; }- let (ba, bb): (Vec<_>, Vec<_>) = (bs.iter().filter(|x| x.part == a).collect(), bs.iter().filter(|x| x.part == b).collect());+ // a body-less SMD test point is flat copper: it stands 0 mm tall (fusion-lane, BQ25792's TP1.. under a module)+ let flat = |r: &str| -> Option<crate::bodies::BodyBox> {+ let ps: Vec<&Value> = pads.iter().filter(|p| p["ref"].as_str() == Some(r)).collect();+ if prefix(r) != "TP" || ps.is_empty() || ps.iter().any(|p| f(&p["drill"]) > 0.0) { return None; }+ let b = pad_rects(r).into_iter().fold((f64::MAX, f64::MAX, f64::MIN, f64::MIN), |a, x| (a.0.min(x.0), a.1.min(x.1), a.2.max(x.2), a.3.max(x.3)));+ Some(crate::bodies::BodyBox { part: r.to_string(), top: true, x0: b.0, y0: b.1, x1: b.2, y1: b.3, z0: 0.0, z1: 0.0, turned: None })+ };+ let (mut ba, mut bb): (Vec<crate::bodies::BodyBox>, Vec<crate::bodies::BodyBox>) = (bs.iter().filter(|x| x.part == a).cloned().collect(), bs.iter().filter(|x| x.part == b).cloned().collect());+ if ba.is_empty() { ba.extend(flat(a)); }+ if bb.is_empty() { bb.extend(flat(b)); } if ba.is_empty() || bb.is_empty() { return false; } // bodies meet when their footprints come within twice the margin (turned ones by their rectangles) and their heights overlap- !ba.iter().any(|p| bb.iter().any(|q| { let (h, d) = crate::bodies::xy_meet(p, q); (h || d < 2.0 * margin) && p.z0.max(q.z0) < p.z1.min(q.z1) }))+ // a flat test point (z 0..0) meets a body that reaches the board (z0 at or below 0)+ !ba.iter().any(|p| bb.iter().any(|q| { let (h, d) = crate::bodies::xy_meet(p, q); (h || d < 2.0 * margin) && (p.z0.max(q.z0) < p.z1.min(q.z1) || (p.z1 == p.z0 && q.z0 <= p.z0) || (q.z1 == q.z0 && p.z0 <= q.z0)) })) } fn prefix(r: &str) -> String { r.chars().take_while(|c| c.is_ascii_alphabetic()).collect::<String>().to_ascii_uppercase() }@@ -311,6 +321,13 @@ mod tests { let real = placement_check(&json!({"board": st, "bodies": {"bodies": [body("M1", -1.0, 11.0, 3.0, 4.0), body("C1", 4.5, 6.5, 0.0, 3.5)]}, "args": {}})); assert_eq!(real["courtyardOverlaps"].as_array().unwrap().len(), 1); assert_eq!(real["ok"], json!(false));+ // a body-less SMD test point under the raised board is flat: stacked; under a body that sits on the board: not+ let st2 = json!({"outline": st["outline"], "outlineEdges": st["outlineEdges"], "components": [{"ref": "M1", "side": "top"}, {"ref": "TP1", "side": "top"}],+ "pads": [pad("M1", 0.0, 0.0), pad("M1", 10.0, 0.0), pad("TP1", 5.0, 0.0)]});+ let tp = placement_check(&json!({"board": st2, "bodies": {"bodies": [body("M1", -1.0, 11.0, 3.0, 4.0)]}, "args": {}}));+ assert_eq!(tp["stacked"][0]["parts"], json!(["M1", "TP1"]), "{tp}");+ let tp_low = placement_check(&json!({"board": st2, "bodies": {"bodies": [body("M1", -1.0, 11.0, -0.05, 1.7)]}, "args": {}}));+ assert_eq!(tp_low["courtyardOverlaps"].as_array().unwrap().len(), 1, "{tp_low}"); // no bodies: pads only, no stack possible to show, nothing hidden let none = placement_check(&json!({"board": st, "args": {}})); assert_eq!(none["bodiesUsed"], json!(false));
engine/src/schreview.rs+103−2@@ -377,8 +377,78 @@ fn sheet_parts(input: &Value) -> Option<Vec<Value>> { p.get("components").or(Some(&p)).and_then(|x| x.as_array()).cloned() } -/// schematic_review: input {sheetParts, netlist}. The netlist companion is {nets: {net: ["U1.8", ...]}, names?: {net:-/// [every label or port name joined into it]}} (or the nets map). Parts may carry value (or comment), libRef (or libId)+type Pt = (f64, f64);+fn pt(v: &Value) -> Option<Pt> { Some((v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)) }+fn d2(a: Pt, b: Pt) -> f64 { ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt() }+fn on_seg(p: Pt, a: Pt, b: Pt) -> f64 {+ let (dx, dy) = (b.0 - a.0, b.1 - a.1);+ let l2 = dx * dx + dy * dy;+ let t = if l2 > 0.0 { (((p.0 - a.0) * dx + (p.1 - a.1) * dy) / l2).clamp(0.0, 1.0) } else { 0.0 };+ d2(p, (a.0 + t * dx, a.1 + t * dy))+}+fn r2(v: f64) -> f64 { (v * 100.0).round() / 100.0 }++/// The checks that need a sheet's drawing (kicad-bridge's sch_check, #29), from the sheetDrawing companion: one entry+/// per sheet {sheet, wires [{a, b}], buses?, junctions [[x, y]], noConnects, labels [{text, kind, at}], pins [{ref,+/// number, at}], symbols [{ref, box: [x0, y0, x1, y1]}]}, mm, y down. A point connects when it lies within 0.01 mm of+/// another item; a wire end on another wire's middle connects only with a junction there. Pure.+pub fn drawing_checks(sheets: &[Value]) -> Vec<Value> {+ const TOL: f64 = 0.01;+ let mut out = Vec::new();+ for sh in sheets {+ let name = sh["sheet"].as_str().unwrap_or("").to_string();+ let list = |k: &str| sh[k].as_array().cloned().unwrap_or_default();+ let wires: Vec<(Pt, Pt)> = list("wires").iter().filter_map(|w| Some((pt(&w["a"])?, pt(&w["b"])?))).collect();+ let points = |k: &str| -> Vec<Pt> { list(k).iter().filter_map(pt).collect() };+ let (junctions, ncs) = (points("junctions"), points("noConnects"));+ let labels: Vec<(String, String, Pt)> = list("labels").iter().filter_map(|l| Some((l["text"].as_str()?.to_string(), l["kind"].as_str().unwrap_or("local").to_string(), pt(&l["at"])?))).collect();+ let pins: Vec<(String, Pt)> = list("pins").iter().filter_map(|p| Some((format!("{}.{}", p["ref"].as_str()?, p["number"].as_str().map(String::from).or_else(|| p["number"].as_i64().map(|n| n.to_string()))?), pt(&p["at"])?))).collect();+ let near = |p: Pt, q: Pt| d2(p, q) <= TOL;+ // dangling wire ends+ for (i, (a, b)) in wires.iter().enumerate() {+ for e in [*a, *b] {+ let joined = pins.iter().any(|x| near(e, x.1)) || labels.iter().any(|x| near(e, x.2)) || junctions.iter().any(|j| near(e, *j)) || ncs.iter().any(|n| near(e, *n))+ || wires.iter().enumerate().any(|(k, (c, d))| k != i && (near(e, *c) || near(e, *d) || (on_seg(e, *c, *d) <= TOL && junctions.iter().any(|j| near(e, *j)))));+ if joined { continue; }+ let mut cand: Vec<(f64, String, Pt)> = pins.iter().map(|x| (d2(e, x.1), format!("pin {}", x.0), x.1)).chain(labels.iter().map(|x| (d2(e, x.2), format!("label {}", x.0), x.2))).filter(|c| c.0 <= 1.27).collect();+ cand.sort_by(|x, y| x.0.total_cmp(&y.0));+ let hint = cand.first().map(|c| format!("; {} is {} mm away at ({}, {})", c.1, r2(c.0), r2(c.2 .0), r2(c.2 .1))).unwrap_or_default();+ out.push(json!({"check": "dangling_wire_end", "sheet": name, "at": [r2(e.0), r2(e.1)], "refs": [], "nets": [],+ "message": format!("Wire end at ({}, {}) connects to nothing{hint}", r2(e.0), r2(e.1)),+ "fix": "Extend the wire onto the pin or label it was meant for (a pin's end, not its middle), or delete the stub."}));+ }+ }+ // labels touching no wire or pin+ for (text, kind, at) in &labels {+ if wires.iter().any(|(a, b)| on_seg(*at, *a, *b) <= TOL) || pins.iter().any(|p| near(*at, p.1)) { continue; }+ out.push(json!({"check": "floating_label", "sheet": name, "at": [r2(at.0), r2(at.1)], "refs": [], "nets": [text], "kind": kind,+ "message": format!("Label {text} at ({}, {}) touches no wire or pin", r2(at.0), r2(at.1)),+ "fix": "Move the label onto its wire (its anchor point must sit on the wire), or delete it: a label off its wire names nothing."}));+ }+ // symbols drawn over each other (more than 0.1 mm both ways)+ let syms: Vec<(String, [f64; 4])> = list("symbols").iter().filter_map(|s| {+ let b = s["box"].as_array()?;+ let v: Vec<f64> = b.iter().filter_map(|x| x.as_f64()).collect();+ (v.len() == 4).then(|| (s["ref"].as_str().unwrap_or("").to_string(), [v[0].min(v[2]), v[1].min(v[3]), v[0].max(v[2]), v[1].max(v[3])]))+ }).collect();+ for i in 0..syms.len() {+ for j in i + 1..syms.len() {+ let (a, b) = (&syms[i].1, &syms[j].1);+ let (ox, oy) = (a[2].min(b[2]) - a[0].max(b[0]), a[3].min(b[3]) - a[1].max(b[1]));+ if ox > 0.1 && oy > 0.1 {+ out.push(json!({"check": "overlap", "sheet": name, "refs": [syms[i].0, syms[j].0], "nets": [],+ "message": format!("{} and {} overlap", syms[i].0, syms[j].0),+ "fix": "Move one of them (sch_edit moveBy) so their bodies and pins are apart; overlapping pins can join nets by accident."}));+ }+ }+ }+ }+ out+}++/// schematic_review: input {sheetParts, netlist, sheetDrawing?}. The netlist companion is {nets: {net: ["U1.8", ...]}, names?: {net:+/// [every label or port name joined into it]}} (or the nets map). sheetDrawing (see drawing_checks) adds the checks+/// that need the drawing; without it they are listed in `unread`. Parts may carry value (or comment), libRef (or libId) /// and unit, which the design checks use. pub fn schematic_review(input: &Value) -> Value { let Some(parts) = sheet_parts(input) else { return crate::input::refuse("bad_args", "pass the sheets' parts (read_sheet_file with pins, every sheet) as sheetParts") };@@ -396,6 +466,15 @@ pub fn schematic_review(input: &Value) -> Value { } } }+ // the checks that need the drawing, when the bridge sends it+ match crate::input::part(input, "sheetDrawing") {+ Some(d) => {+ let sheets = d.get("sheets").or(Some(&d)).and_then(|x| x.as_array()).cloned().unwrap_or_default();+ for f in drawing_checks(&sheets) { out["warnings"].as_array_mut().unwrap().push(f); }+ out["drawingChecked"] = json!(true);+ }+ None => { out["drawingChecked"] = json!(false); out["unread"] = json!(["dangling_wire_end", "floating_label", "overlap"]); }+ } out["success"] = json!(true); out["_hint"] = json!("errors stop a board (a power pin on no net, two outputs on one net, a floating reset or enable, a designator unannotated or used twice); warnings deserve a look (pins on no net, one-pin nets, inputs nothing drives, an IC supply with no capacitor to ground, I2C with no pull-up, an LED with no resistor, a regulator without its capacitors, a no-connect flag on a wired pin, two names on one net); infos are gaps (a rail with no bulk capacitor). Each design finding has message and fix. Pin types come from the symbols: a passive pin is never 'undriven'. Fix on the sheets (sch_connect, sch_place_part), then update_pcb."); out@@ -420,6 +499,28 @@ mod tests { json!({"designator": des, "pins": pins.iter().map(|(n, nm, e)| json!({"number": n, "name": nm, "electrical": e})).collect::<Vec<_>>()}) } + #[test]+ fn the_drawing_checks_find_kicads_examples() {+ let sheet = json!({"sheet": "/", "wires": [{"a": [101.6, 76.2], "b": [127.0, 76.2]}, {"a": [127.0, 76.2], "b": [127.0, 90.0]}, {"a": [60.0, 40.0], "b": [80.0, 40.0]}],+ "junctions": [], "noConnects": [],+ "labels": [{"text": "SDA", "kind": "local", "at": [120.65, 63.5]}, {"text": "SCL", "kind": "local", "at": [70.0, 40.0]}],+ "pins": [{"ref": "U1", "number": "3", "at": [101.6, 75.89]}, {"ref": "R1", "number": "1", "at": [127.0, 90.0]}, {"ref": "R1", "number": "2", "at": [80.0, 40.0]}],+ "symbols": [{"ref": "R3", "box": [10.0, 10.0, 20.0, 15.0]}, {"ref": "C7", "box": [18.0, 12.0, 25.0, 18.0]}, {"ref": "C8", "box": [20.0, 30.0, 25.0, 35.0]}]});+ let f = drawing_checks(&[sheet]);+ let msgs: Vec<&str> = f.iter().map(|x| x["message"].as_str().unwrap()).collect();+ // the wire's left end misses U1.3 by 0.31 mm; the far-left wire starts at (60, 40) on nothing; SDA floats; SCL+ // sits on its wire's middle, so it is fine+ assert!(msgs.contains(&"Wire end at (101.6, 76.2) connects to nothing; pin U1.3 is 0.31 mm away at (101.6, 75.89)"), "{msgs:#?}");+ assert!(msgs.contains(&"Wire end at (60, 40) connects to nothing"), "{msgs:#?}");+ assert!(msgs.contains(&"Label SDA at (120.65, 63.5) touches no wire or pin"));+ assert!(!msgs.iter().any(|m| m.contains("SCL")));+ assert!(msgs.contains(&"R3 and C7 overlap") && !msgs.iter().any(|m| m.contains("C8")));+ assert_eq!(f.len(), 4, "{msgs:#?}");+ // without the drawing the three checks are listed unread+ let r = schematic_review(&json!({"sheetParts": [], "netlist": {"nets": {}}}));+ assert_eq!(r["unread"], json!(["dangling_wire_end", "floating_label", "overlap"]));+ }+ #[test] fn the_design_checks_find_kicads_examples() { let p = |des: &str, lib: &str, value: &str, pins: &[(&str, &str, &str)]| json!({"designator": des, "libRef": lib, "value": value,
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "eda-bridge-standard",- "version": "0.27.2",+ "version": "0.28.0", "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.27.2",+ "standard": "0.28.0", "reference": { "bridge": "altium", "version": "0.2.85"
verbs/standard.json+1−1@@ -1,5 +1,5 @@ {- "standard": "0.27.2",+ "standard": "0.28.0", "reference": { "bridge": "altium", "version": "0.2.85"