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.12.0: engine 0.9.0 adds estimate_cost, prices from a dated record
10 files changed
+200−11
README.md+3@@ -158,6 +158,9 @@ Decisions so far (proposals from the KiCad and Fusion bridges, 2026-10-27; issue ## Status +Version 0.12.0: the engine (0.9.0) adds `estimate_cost` from the KiCad bridge's fab.rs, without its hard-coded rates: prices come+in as a record read from the fab's own page with its date (`prices`), and without one the reply is the board's numbers.+On the Bluetooth Sentinel those numbers match the Altium bridge's own fab_check. Version 0.11.0: the engine (0.8.0) adds `plan_fanout` from the KiCad bridge's place_tools. It also measures each via and stub against every other net's pad on the board (`conflicts`). On the Bluetooth Sentinel's QFNs it plans U1 and U4 clean and names U3's three collisions with nearby passives, the same three an independent check finds.
engine/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-eda-checks"-version = "0.8.0"+version = "0.9.0" dependencies = [ "roxmltree", "serde_json",
engine/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.8.0"+version = "0.9.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+3−1@@ -14,6 +14,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. | `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. From the KiCad bridge's place_tools | | `copper_balance` | board, stack, pours | copper per layer and how evenly mirrored layers match (bow and twist) | | `return_path` | board, stack, pours | signal tracks running over a gap in the neighbouring layer's reference copper, with a pour plan that closes each gap (`apply_pours`) |+| `estimate_cost` | board, stack?, rules?, prices? | the numbers a fab quote needs (size, copper layers, thickness from the stack, smallest track, gap from the clearance rule, smallest drill, holes, SMD and through-hole parts and joints, unique parts, assembled sides) and, with a `prices` record, a rough table at `quantities` (default 5, 10, 30, 100), with assembly when asked, every assumption stated and the record named in `source`. The engine carries no rates: without `prices` the reply is the numbers only. From the KiCad bridge's fab.rs | | `fab_check` | board, stack?, rules?, silk?, fab? | the board's smallest features, compared with a fab's limits from adom/pcb-design-rules; each finding names the fab's page and the date it was read | | `impedance` | stack | the track width (and pair gap) for a target impedance on each copper layer; `apply` answers `add_rule` arguments | | `current_capacity` | board, stack? | per net and layer, the narrowest copper against the width the current needs (IPC-2221), and the vias a layer change needs |@@ -62,6 +63,7 @@ Every check takes one JSON object: | `rules` | the board's rules, `<prefix>_read_rules`: a clearance rule as `{kind: "clearance", clearanceMm}` (a `value` with its unit, `"0.1mm"`, works too). Only copper-to-copper clearance counts: a rule whose kind or name names the board edge, mask, silk, paste, holes, a keepout or restrict area, the courtyard or the same net is skipped | | `silk` | `<prefix>_silk_state` | | `fab` | one record from adom/pcb-design-rules `rules/records/<vendor>.json`, with `vendor` |+| `prices` | a price record read from the fab's own page: `{vendor, url, read, currency, pcb: [{layers, setup, perM2, promo?: {maxMm: [w, h], prices: [[qty, price]]}}], thickness?: {standardMm, factor}, assembly?: {setupPerSide, stencilPerSide, perSmdJoint, perThtJoint, perExtendedPart}, excludes?}`. The pcb row used is the one for the fewest copper layers that covers the board | | `args` | the check's own arguments, as the standard verb documents them | Each part may be the bare model or the bridge's whole reply: the model's keys at the top level, or under `data`.@@ -93,7 +95,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.8.0"`), so a bridge reports the engine it runs without writing the version by hand.+(`"adom-eda-checks 0.9.0"`), so a bridge reports the engine it runs without writing the version by hand. ## Tests
engine/src/cost.rsadded+182@@ -0,0 +1,182 @@+//! The numbers a fab quote needs, from the board model (size, copper layers, thickness, smallest track, gap and drill,+//! holes, parts and joints), and a rough price table at the quantities asked. From the KiCad bridge's fab.rs, with one+//! change: the engine has no rates of its own. Prices come in as a record read from the fab's own page with its date+//! (`prices`), and without one the reply is the numbers only. Pure.+use serde_json::{json, Value};+use std::collections::BTreeSet;++fn f(v: &Value) -> f64 { v.as_f64().unwrap_or(0.0) }+fn r2(v: f64) -> f64 { (v * 100.0).round() / 100.0 }+fn r3(v: f64) -> f64 { (v * 1000.0).round() / 1000.0 }++/// The board's numbers. `layers` from the stack when given, else the copper layers the board uses; the thickness from+/// the stack only.+pub fn board_numbers(st: &Value, stack: Option<&Value>, rules: &[Value]) -> Value {+ let (w, h) = crate::geom::outline_bbox(st).map(|(x0, y0, x1, y1)| (x1 - x0, y1 - y0)).unwrap_or((0.0, 0.0));+ let mut used: BTreeSet<String> = BTreeSet::new();+ for k in ["tracks", "arcs", "pads", "polygons"] {+ for x in st[k].as_array().into_iter().flatten() {+ if let Some(l) = x["layer"].as_str().filter(|l| l.ends_with(".Cu") && *l != "*.Cu") { used.insert(l.to_string()); }+ }+ }+ let layers = stack.and_then(|s| s["copperLayers"].as_u64()).unwrap_or((used.len() as u64).max(2));+ let thickness = stack.and_then(|s| s["totalUm"].as_f64()).filter(|t| *t > 0.0).map(|t| r3(t / 1000.0));+ let m = crate::fab::measure(st);+ let mm = |k: &str| m.get(k).and_then(|v| v["mm"].as_f64());+ let min_drill = [mm("viaDrill"), mm("padHole")].into_iter().flatten().fold(None, |a: Option<f64>, b| Some(a.map(|x| x.min(b)).unwrap_or(b)));+ let pads = st["pads"].as_array().cloned().unwrap_or_default();+ let vias = st["vias"].as_array().map(|a| a.len()).unwrap_or(0);+ let drilled = pads.iter().filter(|p| f(&p["drill"]) > 0.0).count();+ // a part is a component with pads (a logo or a drawing has none); through-hole when any pad is drilled+ let (mut smd, mut tht, mut smd_j, mut tht_j, mut sides, mut unique) = (0, 0, 0, 0, BTreeSet::new(), BTreeSet::new());+ for c in st["components"].as_array().into_iter().flatten() {+ let r = c["ref"].as_str().unwrap_or("");+ let own: Vec<&Value> = pads.iter().filter(|p| p["ref"].as_str() == Some(r)).collect();+ if own.is_empty() { continue; }+ let holes = own.iter().filter(|p| f(&p["drill"]) > 0.0).count();+ if holes > 0 { tht += 1; } else { smd += 1; }+ smd_j += own.len() - holes;+ tht_j += holes;+ sides.insert(c["side"].as_str().unwrap_or("top").to_string());+ unique.insert(c["pattern"].as_str().unwrap_or(r).to_string());+ }+ json!({"widthMm": r3(w), "heightMm": r3(h), "areaMm2": r2(w * h), "layers": layers, "thicknessMm": thickness,+ "minTrackMm": mm("track"), "minGapMm": crate::fab::min_clearance(rules), "minDrillMm": min_drill,+ "holes": vias + drilled, "vias": vias, "drilledPads": drilled, "smdParts": smd, "thtParts": tht, "totalParts": smd + tht,+ "smdJoints": smd_j, "thtJoints": tht_j, "uniqueParts": unique.len(), "assemblySides": sides.into_iter().collect::<Vec<_>>()})+}++/// The price table from a price record: `{vendor, url, read, currency, pcb: [{layers, setup, perM2, promo?: {maxMm: [w,+/// h], prices: [[qty, price]]}}], thickness?: {standardMm, factor}, assembly?: {setupPerSide, stencilPerSide, perSmdJoint,+/// perThtJoint, perExtendedPart}, excludes?: [..]}`. The pcb row is the one for the fewest copper layers that still+/// covers the board.+pub fn price_table(n: &Value, prices: &Value, quantities: &[u64], assembly: bool, basic_parts: Option<u64>) -> Result<Value, String> {+ let layers = n["layers"].as_u64().unwrap_or(2);+ let rows = prices["pcb"].as_array().ok_or("the price record has no pcb rows")?;+ let row = rows.iter().filter(|r| r["layers"].as_u64().map(|l| l >= layers).unwrap_or(false)).min_by_key(|r| r["layers"].as_u64())+ .ok_or_else(|| format!("the price record has no row for {layers} copper layers"))?;+ let (setup, per_m2) = (f(&row["setup"]), f(&row["perM2"]));+ let (w, h) = (f(&n["widthMm"]), f(&n["heightMm"]));+ let area_m2 = w * h / 1_000_000.0;+ let promo = row.get("promo").filter(|p| {+ let max = (f(&p["maxMm"][0]), f(&p["maxMm"][1]));+ (w <= max.0 && h <= max.1) || (w <= max.1 && h <= max.0)+ });+ let factor = match (prices.get("thickness"), n["thicknessMm"].as_f64()) {+ (Some(t), Some(mm)) if (mm - f(&t["standardMm"])).abs() > 0.05 => f(&t["factor"]).max(1.0),+ _ => 1.0,+ };+ let a = &prices["assembly"];+ if assembly && a.is_null() { return Err("assembly was asked for, but the price record has no assembly rates".into()); }+ let sides = n["assemblySides"].as_array().map(|s| s.len()).unwrap_or(1).max(1) as f64;+ let unique = n["uniqueParts"].as_u64().unwrap_or(0);+ let extended = basic_parts.map(|b| unique.saturating_sub(b)).unwrap_or(unique);+ let mut out = Vec::new();+ for &q in quantities {+ let qf = q as f64;+ // the promotional price for the smallest quantity it covers+ let promo_price = promo.and_then(|p| p["prices"].as_array().and_then(|ps| ps.iter().filter(|e| e[0].as_u64().map(|x| x >= q).unwrap_or(false)).min_by_key(|e| e[0].as_u64()).map(|e| f(&e[1]))));+ let pcb = r2(promo_price.unwrap_or(setup + per_m2 * area_m2 * qf) * factor);+ let mut r = json!({"quantity": q, "board": pcb});+ let mut total = pcb;+ if assembly && n["totalParts"].as_u64().unwrap_or(0) > 0 {+ let asm = r2((f(&a["setupPerSide"]) + f(&a["stencilPerSide"])) * sides + (f(&n["smdJoints"]) * f(&a["perSmdJoint"]) + f(&n["thtJoints"]) * f(&a["perThtJoint"])) * qf + f(&a["perExtendedPart"]) * extended as f64);+ r["assembly"] = json!(asm);+ total += asm;+ }+ r["total"] = json!(r2(total));+ r["perBoard"] = json!(r2(total / qf.max(1.0)));+ r["currency"] = prices["currency"].clone();+ out.push(r);+ }+ let mut assumptions = vec![format!("Board: {setup} setup plus {per_m2} per square metre of boards, from the {}-layer row.", row["layers"])];+ if let Some(p) = promo { assumptions.push(format!("Boards up to {} x {} mm take the promotional prices {} where they cover the quantity.", p["maxMm"][0], p["maxMm"][1], p["prices"])); }+ if factor > 1.0 { assumptions.push(format!("A thickness other than {} mm multiplies the board price by {factor}.", prices["thickness"]["standardMm"])); }+ if assembly {+ assumptions.push(format!("Assembly: setup and stencil per assembled side ({sides} side(s)), per joint, and a loading fee per extended part ({extended} of {unique} unique parts counted as extended{}).", if basic_parts.is_some() { "" } else { "; pass basicParts to count the basic ones" }));+ assumptions.push("Component cost is not included: price the BOM with adom-parts-search.".into());+ }+ for e in prices["excludes"].as_array().into_iter().flatten() { if let Some(s) = e.as_str() { assumptions.push(format!("Not included: {s}.")); } }+ Ok(json!({"prices": out, "assumptions": assumptions}))+}++/// estimate_cost: input {board, stack?, rules?, prices?, args: {quantities?, assembly?, basicParts?}}.+pub fn estimate_cost(input: &Value) -> Value {+ let st = match crate::input::board(input) { Ok(b) => b, Err(e) => return e };+ let args = &input["args"];+ let quantities: Vec<u64> = args["quantities"].as_array().map(|a| a.iter().filter_map(|v| v.as_u64()).filter(|q| *q > 0).collect()).unwrap_or_else(|| vec![5, 10, 30, 100]);+ if quantities.is_empty() { return crate::input::refuse("bad_args", "quantities are whole numbers above 0, e.g. [5, 10, 30, 100]"); }+ let assembly = args["assembly"].as_bool().unwrap_or(false);+ let stack = crate::input::stack(input);+ let n = board_numbers(&st, stack.as_ref(), &crate::input::rules(input));+ let mut notes = Vec::new();+ if n["thicknessMm"].is_null() { notes.push("thicknessMm: no stack in the input, so the thickness is unknown and no thickness surcharge is applied"); }+ if n["minGapMm"].is_null() { notes.push("minGapMm: the board's clearance rule, and no rules came in the input"); }+ let Some(prices) = crate::input::part(input, "prices") else {+ return json!({"success": true, "board": n, "prices": Value::Null, "source": Value::Null, "notes": notes, "quantities": quantities,+ "_hint": "The board's numbers only: the engine carries no rates. Pass a price record as prices ({vendor, url, read, currency, pcb: [{layers, setup, perM2, promo?}], thickness?, assembly?, excludes?}), read from the fab's own page with the date, and the reply prices these quantities from it."});+ };+ match price_table(&n, &prices, &quantities, assembly, args["basicParts"].as_u64()) {+ Ok(t) => json!({"success": true, "board": n, "prices": t["prices"], "assumptions": t["assumptions"], "notes": notes, "rough": true,+ "source": {"name": prices["vendor"], "url": prices["url"], "read": prices["read"]},+ "_hint": "A rough estimate from the price record named in source, as of its read date: get a real quote from the fab before ordering."}),+ Err(e) => crate::input::refuse("bad_prices", &e),+ }+}++#[cfg(test)]+mod tests {+ use super::*;++ fn board() -> Value {+ json!({"outline": [[0, 0], [50, 0], [50, 40], [0, 40]],+ "tracks": [{"layer": "F.Cu", "width": 0.2, "net": "A", "start": {"x": 1, "y": 1}, "end": {"x": 5, "y": 1}}, {"layer": "B.Cu", "width": 0.15, "net": "A", "start": {"x": 1, "y": 2}, "end": {"x": 5, "y": 2}}],+ "vias": [{"drill": 0.3, "size": 0.6, "net": "A", "position": {"x": 5, "y": 1.5}}],+ "components": [{"ref": "U1", "side": "top", "pattern": "SOIC8"}, {"ref": "J1", "side": "top", "pattern": "HDR2"}, {"ref": "C1", "side": "bottom", "pattern": "0603"}, {"ref": "LOGO", "side": "top", "pattern": "LOGO"}],+ "pads": [{"ref": "U1", "drill": 0, "layer": "F.Cu"}, {"ref": "U1", "drill": 0, "layer": "F.Cu"}, {"ref": "J1", "drill": 1.0, "layer": "*.Cu", "x": 10, "y": 10, "sizeX": 1.7, "sizeY": 1.7},+ {"ref": "J1", "drill": 1.0, "layer": "*.Cu", "x": 12.54, "y": 10, "sizeX": 1.7, "sizeY": 1.7}, {"ref": "C1", "drill": 0, "layer": "B.Cu"}, {"ref": "C1", "drill": 0, "layer": "B.Cu"}]})+ }++ fn record() -> Value {+ json!({"vendor": "Example Fab", "url": "https://fab.example/prices", "read": "2026-10-29", "currency": "USD",+ "pcb": [{"layers": 2, "setup": 2.0, "perM2": 30.0, "promo": {"maxMm": [100, 100], "prices": [[5, 2.0], [10, 5.0]]}}, {"layers": 4, "setup": 8.0, "perM2": 90.0}],+ "thickness": {"standardMm": 1.6, "factor": 1.15},+ "assembly": {"setupPerSide": 8.0, "stencilPerSide": 7.0, "perSmdJoint": 0.0017, "perThtJoint": 0.0173, "perExtendedPart": 3.0},+ "excludes": ["shipping", "tax"]})+ }++ #[test]+ fn the_numbers_come_from_the_board() {+ let n = board_numbers(&board(), None, &[json!({"kind": "clearance", "clearanceMm": 0.15})]);+ assert_eq!((n["widthMm"].as_f64(), n["heightMm"].as_f64(), n["layers"].as_u64()), (Some(50.0), Some(40.0), Some(2)));+ assert_eq!((n["minTrackMm"].as_f64(), n["minGapMm"].as_f64(), n["minDrillMm"].as_f64()), (Some(0.15), Some(0.15), Some(0.3)));+ // the logo has no pads: three parts, J1 through-hole, two sides+ assert_eq!((n["smdParts"].as_u64(), n["thtParts"].as_u64(), n["smdJoints"].as_u64(), n["thtJoints"].as_u64(), n["holes"].as_u64()), (Some(2), Some(1), Some(4), Some(2), Some(3)));+ assert_eq!(n["assemblySides"], json!(["bottom", "top"]));+ assert!(n["thicknessMm"].is_null());+ assert_eq!(board_numbers(&board(), Some(&json!({"copperLayers": 4, "totalUm": 1600})), &[])["thicknessMm"], json!(1.6));+ }++ #[test]+ fn without_a_price_record_it_answers_the_numbers_only() {+ let r = estimate_cost(&json!({"board": board(), "args": {}}));+ assert_eq!(r["success"], json!(true));+ assert!(r["prices"].is_null() && r["source"].is_null());+ assert_eq!(r["board"]["layers"], json!(2));+ }++ #[test]+ fn prices_come_from_the_record_and_name_it() {+ let r = estimate_cost(&json!({"board": board(), "prices": record(), "args": {"quantities": [5, 30], "assembly": true}}));+ assert_eq!(r["source"], json!({"name": "Example Fab", "url": "https://fab.example/prices", "read": "2026-10-29"}));+ // 5 boards: the promotional 2.00; 30: 2 + 30 * 0.002 m2 * 30 = 3.80+ assert_eq!((r["prices"][0]["board"].as_f64(), r["prices"][1]["board"].as_f64()), (Some(2.0), Some(3.8)), "{r}");+ // assembly at 5: (8 + 7) * 2 sides + (4 * 0.0017 + 2 * 0.0173) * 5 + 3 * 3 unique = 39.21+ assert_eq!(r["prices"][0]["assembly"].as_f64(), Some(39.21), "{r}");+ assert!(r["assumptions"].as_array().unwrap().iter().any(|a| a == "Not included: shipping."));+ // a 4-layer stack takes the 4-layer row; 6 layers has none+ let four = estimate_cost(&json!({"board": board(), "stack": {"copperLayers": 4, "totalUm": 1200}, "prices": record(), "args": {"quantities": [10]}}));+ assert_eq!(four["prices"][0]["board"].as_f64(), Some(r2((8.0 + 90.0 * 0.002 * 10.0) * 1.15)), "{four}");+ assert_eq!(estimate_cost(&json!({"board": board(), "stack": {"copperLayers": 6}, "prices": record(), "args": {}}))["errorCode"], "bad_prices");+ }+}
engine/src/lib.rs+4−2@@ -4,11 +4,12 @@ //! 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?, sheetParts?, netlist?, args?}` (see `input`).+//! Every check takes one input bundle, `{board, stack?, pours?, rules?, silk?, fab?, bodies?, prices?, sheetParts?, netlist?, args?}` (see `input`). use serde_json::Value; pub mod bodies; pub mod coppermap;+pub mod cost; pub mod current; pub mod fab; pub mod features;@@ -25,7 +26,7 @@ pub mod writers; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// The checks this build answers, by standard verb name.-pub const CHECKS: &[&str] = &["check_clearance", "copper_balance", "current_capacity", "fab_check", "height_check", "impedance", "place_decoupling", "placement_check", "plan_fanout", "plan_route", "return_path", "thermal_vias", "via_fence"];+pub const CHECKS: &[&str] = &["check_clearance", "copper_balance", "current_capacity", "estimate_cost", "fab_check", "height_check", "impedance", "place_decoupling", "placement_check", "plan_fanout", "plan_route", "return_path", "thermal_vias", "via_fence"]; /// Run one check or writer by its standard name. The reply carries `engine`. pub fn run(check: &str, input: &Value) -> Value {@@ -40,6 +41,7 @@ fn answer(check: &str, input: &Value) -> Value { "check_clearance" => placetools::check_clearance(input), "copper_balance" => coppermap::copper_balance(input), "current_capacity" => current::current_capacity(input),+ "estimate_cost" => cost::estimate_cost(input), "fab_check" => fab::fab_check(input), "height_check" => bodies::height_check(input), "impedance" => impedance::impedance(input),
engine/src/main.rs+3−3@@ -1,5 +1,5 @@ //! adom-eda-checks <check> [bundle.json | -]-//! adom-eda-checks <check> --board b.json [--stack s.json] [--pours p.json] [--args '{...}']+//! adom-eda-checks <check> --board b.json [--stack s.json] [--pours p.json] [--prices r.json] [--args '{...}'] //! adom-eda-checks import_svg --svg art.svg --args '{"at": [10, 10], "widthMm": 20}' //! Prints the check's reply as JSON. `adom-eda-checks list` names the checks. use serde_json::{json, Value};@@ -23,7 +23,7 @@ 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" | "--sheet-parts" | "--netlist" => {+ "--board" | "--stack" | "--pours" | "--rules" | "--silk" | "--fab" | "--bodies" | "--prices" | "--sheet-parts" | "--netlist" => { let key = match flag.as_str() { "--sheet-parts" => "sheetParts", f => &f[2..] }; b[key] = read(val)?; }@@ -39,7 +39,7 @@ fn bundle(rest: &[String]) -> Result<Value, String> { fn main() { let argv: Vec<String> = std::env::args().skip(1).collect(); let out = match argv.first().map(String::as_str) {- None | Some("-h") | Some("--help") => json!({"success": true, "usage": "adom-eda-checks <check> [bundle.json | -] | <check> --board b.json [--stack s.json] [--pours p.json] [--args '{...}']", "checks": adom_eda_checks::CHECKS, "writers": adom_eda_checks::writers::WRITERS}),+ None | Some("-h") | Some("--help") => json!({"success": true, "usage": "adom-eda-checks <check> [bundle.json | -] | <check> --board b.json [--stack s.json] [--pours p.json] [--prices r.json] [--args '{...}']", "checks": adom_eda_checks::CHECKS, "writers": adom_eda_checks::writers::WRITERS}), Some("list") => json!({"success": true, "checks": adom_eda_checks::CHECKS, "writers": adom_eda_checks::writers::WRITERS}), Some(check) => match bundle(&argv[1..]) { Ok(b) => adom_eda_checks::run(check, &b),
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "eda-bridge-standard",- "version": "0.11.0",+ "version": "0.12.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.11.0",+ "standard": "0.12.0", "reference": { "bridge": "altium", "version": "0.2.79"
verbs/standard.json+1−1@@ -1,5 +1,5 @@ {- "standard": "0.11.0",+ "standard": "0.12.0", "reference": { "bridge": "altium", "version": "0.2.79"