← Commit history

standard 0.20.1: engine 0.17.1 (board_report lcscMissing, tool DRC counts, rule names); fusion 1.13.12

Colby Knox ·b80a93354f ·12d ago ·parent 8ec403e
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.",