← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.12\ No newline at end of file+1.13.13\ No newline at end of file
CHANGELOG.md+8
@@ -3,6 +3,14 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.13.13 (2026-10-29)++- Engine 0.17.2 (standard 0.20.2). The checks get the board's minimum track width and drill (EAGLE's msWidth,+  msDrill) as the standard's width and hole-size rules, and review_board and board_report are told EAGLE's DRC always+  checks shorts: BQ25792's three missing_critical_rules errors were rules Fusion has.+- `fusion_board_report`: the JLCPCB assembly section gets the BOM lines with no LCSC number (as fusion_assembly_check+  does), and the DRC section reads Fusion's violation and airwire counts.+ ## 1.13.12 (2026-10-29)  The standard's pair and length planners, the schematic checks and the design review and report from the shared engine 0.17.0 (standard 0.20.0). 326 verbs.
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.12",+  "version": "1.13.13",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.13.12"+version = "1.13.13" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.12"+version = "1.13.13" edition = "2021"  [dependencies]
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.13.12"+DASH_VERSION="1.13.13" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.12",+  "version": "1.13.13",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+5−11
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.12",+  "version": "1.13.13",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -17,9 +17,7 @@     "pcb",     "exports",     "aps",-    "modeling",-    "mechanical",-    "assemblies"+    "modeling"   ],   "org": "adom",   "title": "Fusion - the Fusion 360 Bridge",@@ -92,13 +90,9 @@     "pin electrical types from a schematic",     "part heights on my fusion board",     "which fusion parts have no 3d model",-    "will my fusion board fit the enclosure",-    "build a parametric part in fusion",-    "assemble mechanical parts in fusion",-    "insert a McMaster part in fusion",-    "generate a mechanical assembly BOM"+    "will my fusion board fit the enclosure"   ],-  "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, author parametric mechanical parts and internal assemblies, ingest McMaster CAD and generate mechanical BOMs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",+  "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",   "sample_prompts": [     {@@ -149,4 +143,4 @@     "name": "John Lauer",     "email": "[email protected]"   }-}\ No newline at end of file+}
rust/Cargo.lock+6−6
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-eda-checks"-version = "0.17.0"+version = "0.17.2" dependencies = [  "roxmltree",  "serde_json",@@ -19,7 +19,7 @@ dependencies = [  [[package]] name = "adom-fusion-bridge"-version = "1.13.12"+version = "1.13.13" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.12"+version = "1.13.13" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.12"+version = "1.13.13" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.12"+version = "1.13.13" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.12"+version = "1.13.13" dependencies = [  "fusion-core",  "windows-sys 0.59.0",
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.13.12"+version = "1.13.13" edition = "2021" license = "MIT" 
rust/bridge/src/checks.rs+3−1
@@ -47,7 +47,7 @@ pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Val             if args[k].as_f64().map_or(true, |v| v < lim) && lim > 0.0 { bundle["args"][k] = json!(lim); }         }     }-    if cmd == "assembly_check" && args["lcscMissing"].is_null() {+    if matches!(cmd, "assembly_check" | "board_report") && args["lcscMissing"].is_null() {         // the BOM lines with no LCSC number, from the board's own part attributes (fusion_board_bom)         let bom = crate::routing::call("board_bom", args, pass);         if let Some(lines) = bom["lines"].as_array() {@@ -56,6 +56,8 @@ pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Val             bundle["args"]["lcscMissing"] = json!(missing);         }     }+    // EAGLE's DRC always checks shorts: there is no rule to find for it+    if matches!(cmd, "review_board" | "board_report") && args["alwaysChecked"].is_null() { bundle["args"]["alwaysChecked"] = json!(["short circuit"]); }     if cmd == "silk_designators" { silk_boxes(&mut bundle["silk"]); }     if cmd == "board_report" {         // every companion this bridge has: the 3D bodies, a fab record (JLCPCB unless vendor names another),
rust/bridge/src/routing_script.py+4
@@ -2960,6 +2960,10 @@ def check_inputs(app, root, board, meta, args):                 std.append({'kind': 'clearance', 'clearanceMm': round(v, 4), 'name': r_.get('name'), 'enabled': True})     dia, drill = _via_rule(board)     std.append({'kind': 'via', 'sizeMm': round(dia, 4), 'drillMm': round(drill, 4), 'name': 'via (drill and restring rules)', 'enabled': True})+    # the board's minimum track width and drill (EAGLE's msWidth, msDrill) in the standard's form: a+    # minimum only, so a planner's default width is not taken from it+    std.append({'kind': 'width', 'minMm': round(_rule_len(board, 'msWidth', 0.2), 4), 'name': 'minimum width (msWidth)', 'enabled': True})+    std.append({'kind': 'hole size', 'minMm': round(_rule_len(board, 'msDrill', 0.3), 4), 'name': 'minimum drill (msDrill)', 'enabled': True})     return {'success': True, 'mutated': False, **meta, 'board': state(board, meta), 'stack': layer_stack(board),             # only the copper clearances: Fusion's other rule kinds also say "Clearance" (Dimension,             # Solder-Mask, Restrict, Keepout) and the engine takes any such kind's value
rust/vendor/adom-eda-checks/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.17.0"+version = "0.17.2" 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"
rust/vendor/adom-eda-checks/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.2"`), so a bridge reports the engine it runs without writing the version by hand.  ## Tests 
rust/vendor/adom-eda-checks/VENDOR.md+3−3
@@ -3,9 +3,9 @@ The EDA Bridge Standard's shared checks engine, copied unchanged from adom/eda-bridge-standard `engine/` (the wiki has no git protocol, so Cargo cannot fetch it). -- Standard version: 0.20.0-- Standard commit: 38b5e58fbad4278de70f9e4cfe258056853810a3-- Crate version: 0.17.0+- Standard version: 0.20.2+- Standard commit: 3c0ef8ff5e5bc11c68ba23f8038dc8d2643eea75+- Crate version: 0.17.2 - Copied: 2026-10-28, by fusion-lane  To update: clone the standard fresh (`adom-wiki repo clone adom/eda-bridge-standard`), copy `engine/src`,
rust/vendor/adom-eda-checks/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}");     } }
rust/vendor/adom-eda-checks/src/review.rs+32−8
@@ -56,14 +56,25 @@ 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(),+        // any other Altium ordinal is no critical rule: its descriptor names it ("Routing Topology Rule (Shortest)" is+        // no short-circuit rule, "Polygon Connect (Conductor Width ...)" no width rule)+        Some(_) => return d.split('(').next().unwrap_or("").trim().to_string(),+        None => {}     }+    // 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 circuit") || t.contains("shortcircuit") { return "short circuit".into(); }+    if (t.contains("width") || t.contains("minimum wire")) && !t.contains("annular") && !t.contains("conductor") { 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 +161,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 +178,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 +208,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 +224,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 +236,12 @@ 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"})), "");+        // Altium's other ordinals stay what their descriptor says, whatever words they hold+        assert_eq!(rule_class(&json!({"kind": 7, "name": "RoutingTopology", "descriptor": "Routing Topology Rule(Topology=Shortest) (All)"})), "routing topology rule");+        assert_eq!(rule_class(&json!({"kind": 6, "name": "PlaneConnect", "descriptor": "Power Plane Connect Rule(Relief Connect )(Conductor Width=0.254mm)"})), "power plane connect rule");     } }