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.20.1: engine 0.17.1 (board_report lcscMissing, tool DRC counts, rule names); fusion 1.13.12
10 files changed
+91−40
README.md+7−2@@ -62,10 +62,10 @@ verb by verb: [docs/coverage.md](docs/coverage.md). |---|---|---|---|---| | **core** | 93 | 93 | 90 | 82 | | **native** | 46 | 46 | 43 | 33 |-| **checks** | 37 | 37 | 35 | 26 |+| **checks** | 37 | 37 | 35 | 32 | | **app** | 62 | 60 | 44 | 45 | | **platform** | 19 | 19 | 0 | 0 |-| standard (all but platform) | 238 | 236 | 212 | 186 |+| standard (all but platform) | 238 | 236 | 212 | 192 | <!-- coverage:end --> The **checks** tier is where the other bridges are furthest behind, and it is the cheapest to close: those verbs are@@ -158,6 +158,11 @@ Decisions so far (proposals from the KiCad and Fusion bridges, 2026-10-27; issue ## Status +Version 0.20.1: the engine (0.17.1) fixes fusion-lane's findings in `board_report`:+- The bridge's BOM gaps reach the assembly section (`lcscMissing`).+- The DRC section reads each tool's own counts (Fusion's `violationCount` and `airwires`).+- `review_board` classes a tool's own rule names and takes `alwaysChecked`.+ Version 0.20.0: the engine (0.17.0) adds `review_board` and `board_report` from the Altium bridge, the last of the checks Fusion asked for. `board_report` runs every engine check on one bundle into a single HTML page, and names the input a section lacks. On the Bluetooth Sentinel the report builds with ten sections. A package-less supply symbol is no part in
docs/coverage.md+9−9@@ -1,6 +1,6 @@ # Coverage: every standard verb on each bridge -Altium bridge 0.2.80 (the reference), KiCad bridge 1.0.82, Fusion 360 bridge 1.13.11. 56 verbs came from other bridges' proposals (tools/proposed/); they are marked *proposed*.+Altium bridge 0.2.80 (the reference), KiCad bridge 1.0.82, Fusion 360 bridge 1.13.12. 56 verbs came from other bridges' proposals (tools/proposed/); they are marked *proposed*. `exact` is the same job under the same or an equivalent name; `partial` does part of it or does it differently (`verbs/coverage.json` names what in `note`); `unsupported` answers `not_supported` for the whole job and is not counted; blank is missing. Generated by `tools/generate.py` from `verbs/coverage.json`; do not edit by hand. @@ -8,10 +8,10 @@ Generated by `tools/generate.py` from `verbs/coverage.json`; do not edit by hand |---|---|---|---|---| | **core** | 93 | 93 | 90 | 82 | | **native** | 46 | 46 | 43 | 33 |-| **checks** | 37 | 37 | 35 | 26 |+| **checks** | 37 | 37 | 35 | 32 | | **app** | 62 | 60 | 44 | 45 | | **platform** | 19 | 19 | 0 | 0 |-| standard (all but platform) | 238 | 236 | 212 | 186 |+| standard (all but platform) | 238 | 236 | 212 | 192 | ## core @@ -175,7 +175,7 @@ Engineering computed from the board model, not by the tool: one shared engine, s | `assembly_check` | Ready for JLCPCB assembly? The board against both JLCPCB PCBA tiers as published (Economic and Standard): board or panel size, thickness and layers, parts... | `altium_assembly_check` | `kicad_assembly_check` | `fusion_assembly_check` | | `auto_classes` | Net classes from the nets' own names: GND, Power (VCC, VDD, 3V3, VBAT...), Clock, HighSpeed (USB, Ethernet, HDMI, PCIe, DDR, MIPI, LVDS, SATA...) and the... | `altium_auto_classes` | `kicad_auto_classes` | `fusion_auto_classes` | | `board_features` | Add the furniture every real board needs in one undo step: mounting holes (at points or the four corners), fiducials, keepout areas, and a stitching-via... | `altium_board_features` | `kicad_add_mounting_hole` (partial) | `fusion_board_features` (partial) |-| `board_report` | One shareable page per board: every check run against the live board and its project (Altium's DRC, JLCPCB fab limits and assembly tiers, the schematic... | `altium_board_report` | `kicad_board_report` | |+| `board_report` | One shareable page per board: every check run against the live board and its project (Altium's DRC, JLCPCB fab limits and assembly tiers, the schematic... | `altium_board_report` | `kicad_board_report` | `fusion_board_report` | | `bom` | The project's bill of materials read straight from Altium: every schematic sheet the .PrjPcb lists (placed parts and their parameters) merged with the board... | `altium_bom` | `kicad_bom` | `fusion_board_bom` (partial) | | `build_board` | Build a NEW board from a parts list and a netlist: outline (rounded corners allowed), every footprint from one PcbLib placed by its own origin, pad nets... | `altium_build_board` | `kicad_build_board` | | | `build_footprint` | Build a complete footprint (pads, silkscreen, courtyard, arcs) in a PcbLib from a kicad2altium spec file on this machine, and return a proof frame. | `altium_build_footprint` | | |@@ -193,18 +193,18 @@ Engineering computed from the board model, not by the tool: one shared engine, s | `impedance` | Controlled impedance from the board's own stack-up: the track width each copper layer needs for a single-ended target (50 ohm default) and the width and gap... | `altium_impedance` | `kicad_impedance` | `fusion_impedance` | | `import_part` | One call imports a whole part: footprint spec + STEP + symbol modes + models + params + save. | `altium_import_part` | | | | `jlcpcb_order` | A JLCPCB order in one call: the board checked against JLCPCB's fab limits, the Gerbers and drill zipped the way JLCPCB's order page takes them, the JLCPCB... | `altium_jlcpcb_order` | `kicad_jlcpcb_order` | `fusion_jlcpcb_order` |-| `length_tune` | Lengthen a routed net to a target length, or to match another net (a pair's skew, a DDR byte lane): a serpentine in its longest straight track that has room... | `altium_length_tune` | `kicad_length_tune` | |+| `length_tune` | Lengthen a routed net to a target length, or to match another net (a pair's skew, a DDR byte lane): a serpentine in its longest straight track that has room... | `altium_length_tune` | `kicad_length_tune` | `fusion_length_tune` | | `net_lengths` | Routed length of every net (or the ones named) on the live board, by layer with its vias; the skew inside each differential pair; and the spread of... | `altium_net_lengths` | `kicad_net_lengths` | `fusion_net_lengths` |-| `pin_pad_check` | Do the footprints fit their symbols? Part by part, the symbol's pin numbers (from the sheet files) against the footprint's pad names (from the board): pins... | `altium_pin_pad_check` | `kicad_pin_pad_check` | |+| `pin_pad_check` | Do the footprints fit their symbols? Part by part, the symbol's pin numbers (from the sheet files) against the footprint's pad names (from the board): pins... | `altium_pin_pad_check` | `kicad_pin_pad_check` | `fusion_pin_pad_check` | | `place_decoupling` *proposed* | Plan positions for decoupling capacitors beside an IC's power pins (each cap just outside the courtyard, its power pad facing the pin), answered as... | `altium_place_decoupling` | `kicad_place_decoupling` | `fusion_place_decoupling` | | `placement_check` | Review the placement before routing: parts whose courtyards (pads and body, grown by a margin) overlap on the same side, parts off the board or over its... | `altium_placement_check` | `kicad_placement_check` | `fusion_placement_check` | | `plan_fanout` *proposed* | Plan the fanout of a BGA or QFN/QFP: the pad pitch, dogbone or via-in-pad per pad, the escape layer, surface stubs for the outer ring and a via grid for an... | `altium_plan_fanout` | `kicad_plan_fanout` | `fusion_plan_fanout` | | `plan_route` *proposed* | Plan a path for one net between two pads without changing the board: a grid search around the existing copper that keeps the clearance rules, on the allowed... | `altium_plan_route` | `kicad_plan_route` | `fusion_plan_route` | | `read_sheet_file` | Read a schematic sheet straight from its .SchDoc file, without Altium (it need not be installed or running): every part with its designator, library... | `altium_read_sheet_file` | `kicad_read_sheet_file` | `fusion_read_sheet_file` | | `return_path` | Where signal tracks lose their reference: for every signal net, the length of track on each layer that runs over no pour or plane copper on either... | `altium_return_path` | `kicad_return_path` | `fusion_return_path` |-| `review_board` | Design-review checks (14) over one board read; schematic checks with includeSchematic. | `altium_review_board` | `kicad_lint_board` (partial) | |-| `route_pair` | Route a differential pair as a pair: one centreline from the source pads' midpoint through waypoints to the destination pads' midpoint, each net offset to... | `altium_route_pair` | `kicad_route_pair` | |-| `schematic_review` | Review the schematic before Update PCB: every placed pin (read from the sheet files, with its electrical type) against Altium's compiled netlist. Errors:... | `altium_schematic_review` | `kicad_sch_check / kicad_design_review / kicad_lint_schematic` (partial) | |+| `review_board` | Design-review checks (14) over one board read; schematic checks with includeSchematic. | `altium_review_board` | `kicad_lint_board` (partial) | `fusion_review_board` |+| `route_pair` | Route a differential pair as a pair: one centreline from the source pads' midpoint through waypoints to the destination pads' midpoint, each net offset to... | `altium_route_pair` | `kicad_route_pair` | `fusion_route_pair` |+| `schematic_review` | Review the schematic before Update PCB: every placed pin (read from the sheet files, with its electrical type) against Altium's compiled netlist. Errors:... | `altium_schematic_review` | `kicad_sch_check / kicad_design_review / kicad_lint_schematic` (partial) | `fusion_schematic_review` | | `silk_designators` | Place every reference designator the way assembly houses want: one size, upright, beside its own part, clear of every pad (JLCPCB: 0.15 mm), every other... | `altium_silk_designators` | `kicad_silk_designators` | `fusion_silk_designators` | | `test_board` | Place one footprint, or a ';' list, from the open PcbLib on ONE board: a NEW board (wMm x hMm outline) when pcbPath is new or omitted, APPENDED to the... | `altium_test_board` | `kicad_test_board` | | | `thermal_vias` | A thermal via grid in a part's exposed pad (the ground or thermal pad under a QFN, DFN, PowerPAD or regulator): centred, inset from the pad's edge, sized by... | `altium_thermal_vias` | `kicad_thermal_vias` | `fusion_thermal_vias` |
engine/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-eda-checks"-version = "0.17.0"+version = "0.17.1" dependencies = [ "roxmltree", "serde_json",
engine/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.17.0"+version = "0.17.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+3−3@@ -13,7 +13,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. |---|---|---| | `assembly_check` | board, stack? | the board against JLCPCB's two PCBA tiers as published (with the page and its read date): size (or `panelMm`), thickness and layers from the stack, one or two sides, the smallest two-pad part and the finest IC pitch, fiducials, and parts without an LCSC number (`lcscMissing` from the bridge's BOM). Pads sharing one centre are one land, and a component with no pads is no part. `service` is the cheaper tier every rule allows. Matches the Altium bridge's answers on its test boards. From its assembly.rs | | `auto_classes` | board | net classes from the names the nets already have (GND, Power, Clock, HighSpeed) and differential pairs from their suffixes (`_P`/`_N`, `DP`/`DN`, `+`/`-`), with `applyArgs`: the net_classes and diff_pairs calls that write them (Power with a `powerWidthMm` width rule). From the Altium bridge's autoclass.rs |-| `board_report` | board, and every companion it can use | every check the engine has, run on this bundle, as one self-contained HTML page (`text`), dark by default: fab limits (with `fab`), JLCPCB assembly, schematic review and symbols against footprints (with `sheetParts` and `netlist`), impedance, return paths and copper balance (with `stack`), 3D heights (with `bodies`), the design review, and the tool's own DRC (`drc`) and a base64 PNG of the board (`picture`) when the bridge passes them. A section whose input is missing says which to pass. From the Altium bridge's report.rs |+| `board_report` | board, and every companion it can use | every check the engine has, run on this bundle, as one self-contained HTML page (`text`), dark by default: fab limits (with `fab`), JLCPCB assembly, schematic review and symbols against footprints (with `sheetParts` and `netlist`), impedance, return paths and copper balance (with `stack`), 3D heights (with `bodies`), the design review, and the tool's own DRC (`drc`) and a base64 PNG of the board (`picture`) when the bridge passes them; `args` carries `lcscMissing`, `panelMm`, `gridMm` and `alwaysChecked` to those checks. A section whose input is missing says which to pass. From the Altium bridge's report.rs | | `check_clearance` | board, bodies? | pairs of parts closer than `thresholdMm`, closest first: `mode: courtyard` (default 0.25) measures part boxes (pads, and 3D bodies when given) on the same side; `mode: pads` (default 0.2) measures pad copper of different parts that shares a layer, by its real outline, skipping same-net pairs unless `includeSameNet`; `refs` keeps pairs involving those parts. A part under a raised one (pads apart, bodies apart in 3D) is listed in `stacked`, not `pairs`, as placement_check lists it. From the KiCad bridge's place_tools | | `check_orientation` | board | advisory: each two-pad part's quarter turns scored by airline length (MST of a routable net with the pad there, distance to the nearest pad of a plane-like net such as GND); turns saving `minImprovementMm` (0.6) or more are suggested, nothing moves. From the Altium bridge's design.rs | | `copper_balance` | board, stack, pours | copper per layer and how evenly mirrored layers match (bow and twist) |@@ -31,7 +31,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. | `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`); 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. `skipped` and `notImplemented` say what was not judged. From the Altium bridge's review.rs |+| `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 | | `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 |@@ -112,7 +112,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.17.0"`), so a bridge reports the engine it runs without writing the version by hand.+(`"adom-eda-checks 0.17.1"`), so a bridge reports the engine it runs without writing the version by hand. ## Tests
engine/src/report.rs+15−5@@ -24,8 +24,12 @@ pub fn sections(c: &Value) -> Vec<Section> { // design rules let d = &c["drc"]; out.push(if d.is_null() { Section { title: "Design rules (the tool's DRC)".into(), ok: None, summary: "not run: pass the tool's DRC reply (run_drc) as drc".into(), rows: vec![] } } else if failed(d) { err("Design rules (the tool's DRC)", d) } else {- Section { title: "Design rules (the tool's DRC)".into(), ok: Some(d["errors"].as_i64() == Some(0) && d["unconnected"].as_i64() == Some(0)),- summary: format!("{} violations, {} unconnected", txt(&d["errors"]), txt(&d["unconnected"])),+ // each tool counts in its own words: Altium errors/unconnected, Fusion violationCount/airwires+ let errs = d["errors"].as_i64().or(d["violationCount"].as_i64());+ let open = d["unconnected"].as_i64().or(d["airwires"].as_i64()).or(d["unrouted"].as_i64());+ let shown = |v: Option<i64>| v.map(|n| n.to_string()).unwrap_or_else(|| "?".into());+ Section { title: "Design rules (the tool's DRC)".into(), ok: match (errs, open) { (Some(e), Some(u)) => Some(e == 0 && u == 0), (Some(e), None) if e > 0 => Some(false), _ => None },+ summary: format!("{} violations, {} unconnected", shown(errs), shown(open)), rows: d["violations"].as_array().cloned().unwrap_or_default().iter().take(30).map(|v| (txt(&v["rule"]), txt(&v["objects"]))).collect() } }); let f = &c["fab"]; // the vendor as each finding's source names it, else the record's own name@@ -104,7 +108,7 @@ table{{width:100%;border-collapse:collapse;font-size:13px}} th,td{{text-align:le } /// board_report: input {board, stack?, pours?, rules?, bodies?, fab?, sheetParts?, netlist?, drc?, picture?, args: {title?,-/// date?}}. Every check the engine has, run on this bundle, as one self-contained HTML page (`text`), with the tool's+/// date?, lcscMissing?, panelMm?, gridMm?, alwaysChecked?}}; the last four go to assembly_check and review_board. Every check the engine has, run on this bundle, as one self-contained HTML page (`text`), with the tool's /// own DRC reply (`drc`) and a base64 PNG of the board (`picture`) when the bridge passes them. A section whose input /// is missing says so instead of passing. pub fn board_report(input: &Value) -> Value {@@ -115,12 +119,13 @@ pub fn board_report(input: &Value) -> Value { c["schematic"] = need("sheetParts", "the sheets' parts").or_else(|| need("netlist", "the compiled netlist")).unwrap_or_else(|| crate::run("schematic_review", &with(json!({})))); c["pinPad"] = need("sheetParts", "the sheets' parts").unwrap_or_else(|| crate::run("pin_pad_check", &with(json!({})))); c["fab"] = need("fab", "a pcb-design-rules record").unwrap_or_else(|| crate::run("fab_check", &with(json!({}))));- c["assembly"] = crate::run("assembly_check", &with(json!({})));+ // the bridge's BOM lines without an LCSC number, when it passes them (fusion-lane: without, the tier read economic)+ c["assembly"] = crate::run("assembly_check", &with(json!({"lcscMissing": input["args"]["lcscMissing"], "panelMm": input["args"]["panelMm"]}))); c["impedance"] = need("stack", "the stack-up").unwrap_or_else(|| crate::run("impedance", &with(json!({"z0": 50, "diff": 90})))); c["returnPath"] = need("stack", "the stack-up").unwrap_or_else(|| crate::run("return_path", &with(json!({})))); c["balance"] = need("stack", "the stack-up").unwrap_or_else(|| crate::run("copper_balance", &with(json!({})))); c["heights"] = need("bodies", "the 3D bodies").unwrap_or_else(|| crate::run("height_check", &with(json!({}))));- c["review"] = crate::run("review_board", &with(json!({})));+ c["review"] = crate::run("review_board", &with(json!({"gridMm": input["args"]["gridMm"], "alwaysChecked": input["args"]["alwaysChecked"]}))); c["drc"] = crate::input::part(input, "drc").unwrap_or(Value::Null); let secs = sections(&c); let args = &input["args"];@@ -166,5 +171,10 @@ mod tests { assert!(sec("Fab limits")["summary"].as_str().unwrap().contains("not run")); assert_eq!(sec("Design review")["ok"], json!(true), "{r}"); assert!(sec("JLCPCB assembly")["summary"].as_str().unwrap().starts_with("tier"));+ // Fusion's DRC counts in its own words; the bridge's LCSC gaps reach the assembly section+ let f = board_report(&json!({"board": st, "drc": {"violationCount": 3, "airwires": 0}, "args": {"lcscMissing": 2}}));+ let sec = |t: &str| f["sections"].as_array().unwrap().iter().find(|s| s["title"].as_str().unwrap_or("").starts_with(t)).cloned().unwrap();+ assert_eq!((sec("Design rules")["summary"].clone(), sec("Design rules")["ok"].clone()), (json!("3 violations, 0 unconnected"), json!(false)));+ assert_eq!(sec("JLCPCB assembly")["ok"], json!(false), "{f}"); } }
engine/src/review.rs+26−8@@ -56,14 +56,22 @@ pub fn rule_class(r: &Value) -> String { if d.starts_with(prefix) { return c.into(); } } match r["kind"].as_i64() {- Some(0) => "clearance".into(), Some(2) => "width".into(), Some(42) => "hole size".into(), Some(15) => "short circuit".into(),- _ => d.split('(').next().unwrap_or("").trim().to_string(),+ Some(0) => return "clearance".into(), Some(2) => return "width".into(), Some(42) => return "hole size".into(), Some(15) => return "short circuit".into(),+ _ => {} }+ // a tool's own rule names (Fusion's EAGLE rules: wire-wire, pad-via, minimum width, minimum drill, ...)+ let t = format!("{} {} {}", s(r, "kindName"), s(r, "name"), d).to_lowercase().replace(['_', '-'], " ");+ if t.contains("short") { return "short circuit".into(); }+ if t.contains("width") && !t.contains("annular") { return "width".into(); }+ if t.contains("drill") || t.contains("hole size") || t.contains("holesize") { return "hole size".into(); }+ let copper = !["silk", "mask", "paste", "edge", "board", "courtyard", "component", "hole to hole", "plane"].iter().any(|w| t.contains(w));+ if copper && (t.contains("clearance") || t.contains("wire wire") || t.contains("pad via") || t.contains("pad pad")) { return "clearance".into(); }+ d.split('(').next().unwrap_or("").trim().to_string() } /// The checks over the board model, the sheet parts (read_sheet_file's components, or none) and the rules. Returns the /// findings (errors first), the checks run and those skipped with why. Pure.-pub fn run_checks(st: &Value, sheet: Option<&[Value]>, rules: &[Value], grid: f64, edge: f64, wanted: Option<&[String]>) -> (Vec<Value>, Vec<String>, Map<String, Value>) {+pub fn run_checks(st: &Value, sheet: Option<&[Value]>, rules: &[Value], grid: f64, edge: f64, wanted: Option<&[String]>, always: &[String]) -> (Vec<Value>, Vec<String>, Map<String, Value>) { let comps = st["components"].as_array().cloned().unwrap_or_default(); let pads = st["pads"].as_array().cloned().unwrap_or_default(); let ring = crate::coppermap::outline_ring(st);@@ -150,7 +158,9 @@ pub fn run_checks(st: &Value, sheet: Option<&[Value]>, rules: &[Value], grid: f6 finding("disabled_design_rules", sev, ("rule", s(r, "name")), format!("Design rule {:?} ({c}) is disabled: DRC passes without ever applying it.", s(r, "name"))) }).collect()); run("missing_critical_rules", no_rules, &mut out, &|| {- let have: BTreeSet<String> = rules.iter().filter(|r| r["enabled"] != json!(false)).map(rule_class).collect();+ // a tool that always checks a class (EAGLE's DRC always checks shorts) passes it in alwaysChecked+ let mut have: BTreeSet<String> = rules.iter().filter(|r| r["enabled"] != json!(false)).map(rule_class).collect();+ have.extend(always.iter().map(|a| a.to_lowercase().replace(['_', '-'], " "))); CRITICAL.iter().filter(|c| !have.contains(**c)).map(|c| finding("missing_critical_rules", "error", ("rule", c), format!("No enabled {c} rule on this board."))).collect() }); run("duplicate_rule_priority", no_rules, &mut out, &|| {@@ -165,13 +175,14 @@ pub fn run_checks(st: &Value, sheet: Option<&[Value]>, rules: &[Value], grid: f6 (out, ran, skipped) } -/// review_board: input {board, sheetParts?, rules?, args: {checks?, gridMm?, edgeMm?}}.+/// review_board: input {board, sheetParts?, rules?, args: {checks?, gridMm?, edgeMm?, alwaysChecked?: ["short circuit", ...]}}. pub fn review_board(input: &Value) -> Value { let st = match crate::input::board(input) { Ok(b) => b, Err(e) => return e }; let args = &input["args"]; let sheet: Option<Vec<Value>> = crate::input::part(input, "sheetParts").and_then(|p| p.get("components").or(Some(&p)).and_then(|x| x.as_array()).cloned()); let wanted: Option<Vec<String>> = args["checks"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect());- let (findings, ran, skipped) = run_checks(&st, sheet.as_deref(), &crate::input::rules(input), args["gridMm"].as_f64().unwrap_or(0.0), args["edgeMm"].as_f64().unwrap_or(0.5), wanted.as_deref());+ let always: Vec<String> = args["alwaysChecked"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect()).unwrap_or_default();+ let (findings, ran, skipped) = run_checks(&st, sheet.as_deref(), &crate::input::rules(input), args["gridMm"].as_f64().unwrap_or(0.0), args["edgeMm"].as_f64().unwrap_or(0.5), wanted.as_deref(), &always); let mut counts = json!({"error": 0, "warning": 0, "info": 0}); for x in &findings { let k = s(x, "severity").to_string(); counts[&k] = json!(counts[&k].as_i64().unwrap_or(0) + 1); } json!({"success": true, "findings": findings, "counts": counts, "checks": ran, "skipped": skipped,@@ -194,7 +205,7 @@ mod tests { let rules = vec![json!({"name": "Clearance", "kind": 0, "enabled": true, "priority": 1, "descriptor": "Clearance Constraint (Gap=0.1mm)"}), json!({"name": "Short", "kind": 15, "enabled": false, "priority": 1, "descriptor": "Short-Circuit Constraint (Allowed=No)"}), json!({"name": "W1", "kind": "width", "enabled": true, "priority": 2}), json!({"name": "W2", "kind": "width", "enabled": true, "priority": 2})];- let (f, ran, _) = run_checks(&st, Some(&sheet), &rules, 0.127, 0.5, None);+ let (f, ran, _) = run_checks(&st, Some(&sheet), &rules, 0.127, 0.5, None, &[]); let has = |c: &str, key: &str, v: &str| f.iter().any(|x| x["check"] == c && x[key] == v); assert!(has("placeholder_values", "designator", "U1")); assert!(has("single_pin_nets", "net", "LONE"));@@ -210,8 +221,11 @@ mod tests { assert!(f.iter().any(|x| x["check"] == "duplicate_rule_priority")); assert_eq!(f[0]["severity"], "error"); assert_eq!(ran.len(), 13);- let (_, _, skipped) = run_checks(&st, None, &[], 0.127, 0.5, None);+ let (_, _, skipped) = run_checks(&st, None, &[], 0.127, 0.5, None, &[]); assert!(skipped.contains_key("vendor_part_numbers") && skipped.contains_key("missing_critical_rules"));+ // EAGLE always checks shorts: passed in alwaysChecked, a missing short-circuit rule is no finding+ let (f2, _, _) = run_checks(&st, Some(&sheet), &rules, 0.0, 0.5, Some(&["missing_critical_rules".to_string()]), &["short_circuit".to_string()]);+ assert!(!f2.iter().any(|x| x["rule"] == "short circuit"), "{f2:?}"); } #[test]@@ -219,5 +233,9 @@ mod tests { assert_eq!(rule_class(&json!({"kind": 42})), "hole size"); assert_eq!(rule_class(&json!({"kind": 99, "descriptor": "Width Constraint (Min=0.1mm)"})), "width"); assert_eq!(rule_class(&json!({"kind": "short_circuit"})), "short circuit");+ // a tool's own names (Fusion's EAGLE rules), and a silk clearance is no copper clearance+ assert_eq!(rule_class(&json!({"name": "Wire-Wire", "value": "0.1mm"})), "clearance");+ assert_eq!(rule_class(&json!({"name": "Minimum Drill"})), "hole size");+ assert_eq!(rule_class(&json!({"name": "Silk Clearance"})), ""); } }
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "eda-bridge-standard",- "version": "0.20.0",+ "version": "0.20.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+26−8@@ -1,5 +1,5 @@ {- "standard": "0.20.0",+ "standard": "0.20.1", "reference": { "bridge": "altium", "version": "0.2.80"@@ -7,7 +7,7 @@ "proposed": 56, "compared": { "kicad": "1.0.82",- "fusion360": "1.13.11"+ "fusion360": "1.13.12" }, "tiers": { "core": "Read and edit the design. Every bridge provides these: they are how an AI sees and changes a board.",@@ -2392,7 +2392,10 @@ "verb": "kicad_board_report", "match": "exact" },- "fusion360": null+ "fusion360": {+ "verb": "fusion_board_report",+ "match": "exact"+ } }, { "name": "bom",@@ -2677,7 +2680,10 @@ "verb": "kicad_length_tune", "match": "exact" },- "fusion360": null+ "fusion360": {+ "verb": "fusion_length_tune",+ "match": "exact"+ } }, { "name": "net_lengths",@@ -2708,7 +2714,10 @@ "verb": "kicad_pin_pad_check", "match": "exact" },- "fusion360": null+ "fusion360": {+ "verb": "fusion_pin_pad_check",+ "match": "exact"+ } }, { "name": "place_decoupling",@@ -2824,7 +2833,10 @@ "verb": "kicad_lint_board", "match": "partial" },- "fusion360": null+ "fusion360": {+ "verb": "fusion_review_board",+ "match": "exact"+ } }, { "name": "route_pair",@@ -2838,7 +2850,10 @@ "verb": "kicad_route_pair", "match": "exact" },- "fusion360": null+ "fusion360": {+ "verb": "fusion_route_pair",+ "match": "exact"+ } }, { "name": "schematic_review",@@ -2852,7 +2867,10 @@ "verb": "kicad_sch_check / kicad_design_review / kicad_lint_schematic", "match": "partial" },- "fusion360": null+ "fusion360": {+ "verb": "fusion_schematic_review",+ "match": "exact"+ } }, { "name": "silk_designators",
verbs/standard.json+2−2@@ -1,5 +1,5 @@ {- "standard": "0.20.0",+ "standard": "0.20.1", "reference": { "bridge": "altium", "version": "0.2.80"@@ -7,7 +7,7 @@ "proposed": 56, "compared": { "kicad": "1.0.82",- "fusion360": "1.13.11"+ "fusion360": "1.13.12" }, "tiers": { "core": "Read and edit the design. Every bridge provides these: they are how an AI sees and changes a board.",