app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
main
John Lauer
0.2.0: the bridge is Rust end to end — all 112 verbs dispatch from altium-bridge.exe (spawn.kind exe); Python runtime n…
4fb2c47
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
//! Design review: 14 checks, each a PURE FUNCTION over data the bridge already reads — adding
//! one costs no new Altium API surface (a wrong identifier in the agent halts the script engine
//! behind a modal on the user's screen). Checks needing geometry Altium has not been asked for
//! (outline polygon, silkscreen overlap, ratsnest) are deliberately absent rather than guessed;
//! `notImplemented` in the reply names them so nobody mistakes silence for a clean board.
use crate::board::natural_cmp;
use crate::support::{self, timeout_of};
use crate::agent;
use serde_json::{json, Value};
const PLACEHOLDER_VALUES: [&str; 12] = ["comment", "value", "*", "?", "n/a", "na", "tbd", "xxx",
"component", "designator", "part", "unknown"];
/// Adom house rule (John, standing): Mouser is the PREFERRED vendor, and every component carries
/// Mouser + DigiKey + LCSC part numbers.
const VENDOR_PATTERNS: [(&str, &[&str]); 3] = [
("Mouser", &["mouser"]),
("DigiKey", &["digikey", "digi-key", "digi key"]),
("LCSC", &["lcsc", "jlcpcb"]),
];
/// Designator prefixes that legitimately carry no vendor part number.
const NON_PURCHASED_PREFIXES: [&str; 8] = ["TP", "FID", "MH", "H", "LOGO", "NET", "PWR", "GND"];
/// Rules whose absence a fabricator will notice before you do: (kind ordinal, name).
const CRITICAL_RULE_KINDS: [(i64, &str); 4] = [(0, "Clearance"), (2, "Width"), (42, "HoleSize"), (15, "ShortCircuit")];
pub const NOT_IMPLEMENTED: [(&str, &str); 6] = [
("pads_near_board_edge", "needs per-PAD geometry; components_near_board_edge measures component origins instead, which is a weaker claim"),
("outline_shape_checks", "the outline read carries a BOUNDING BOX, not the polygon — a part inside the box but outside an L-shaped or slotted outline is not caught"),
("unrouted_connections", "needs the ratsnest"),
("silkscreen_over_pad", "needs per-layer primitive geometry"),
("via_antennas", "needs via/track topology"),
("missing_decoupling", "needs net-to-part topology"),
];
// ---- data access helpers -----------------------------------------------------
fn components(src: &Value) -> Vec<Value> {
src.get("components").and_then(|c| c.get("components")).and_then(|a| a.as_array()).cloned().unwrap_or_default()
}
fn str_of<'a>(v: &'a Value, k: &str) -> &'a str {
v.get(k).and_then(|x| x.as_str()).unwrap_or("")
}
fn finding(check: &str, severity: &str, key: (&str, &str), message: String) -> Value {
let mut f = json!({"check": check, "severity": severity, "message": message});
if !key.1.is_empty() {
f[key.0] = json!(key.1);
}
f
}
fn is_purchased(designator: &str) -> bool {
let prefix: String = designator.to_uppercase().chars().filter(|c| c.is_ascii_alphabetic()).collect();
!NON_PURCHASED_PREFIXES.contains(&prefix.as_str())
}
fn params_of(comp: &Value) -> Vec<(String, String)> {
comp.get("params").and_then(|p| p.as_array()).map(|a| {
a.iter().filter_map(|p| {
let name = p.get("name").and_then(|n| n.as_str())?.trim().to_lowercase();
let value = p.get("value").and_then(|v| v.as_str()).unwrap_or("").trim().to_string();
if name.is_empty() { None } else { Some((name, value)) }
}).collect()
}).unwrap_or_default()
}
// ---- the checks --------------------------------------------------------------
/// error — a component still carrying Altium's default text instead of a real value/MPN.
fn chk_placeholder_values(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
components(board).iter().filter_map(|c| {
let com = str_of(c, "comment").trim().to_string();
if PLACEHOLDER_VALUES.contains(&com.to_lowercase().as_str()) {
Some(finding("placeholder_values", "error", ("designator", str_of(c, "designator")),
format!("{} has placeholder Comment {com:?} — it will reach the BOM as-is.", str_of(c, "designator"))))
} else { None }
}).collect()
}
/// warning — a net with exactly one pad connects to nothing.
fn chk_single_pin_nets(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
board.get("nets").and_then(|n| n.get("nets")).and_then(|a| a.as_array()).map(|nets| {
nets.iter().filter_map(|n| {
if n.get("pads").and_then(|p| p.as_i64()) == Some(1) {
let name = str_of(n, "name");
Some(finding("single_pin_nets", "warning", ("net", name),
format!("Net {name:?} has a single pad — it connects to nothing.")))
} else { None }
}).collect()
}).unwrap_or_default()
}
/// info — a mechanical layer that is enabled but carries nothing.
fn chk_unused_mechanical_layers(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
let occupied: std::collections::BTreeSet<String> = board.get("occupancy").and_then(|o| o.get("occupancy"))
.and_then(|a| a.as_array())
.map(|a| a.iter().filter_map(|l| l.get("layerId").and_then(|i| i.as_str()).map(str::to_string)).collect())
.unwrap_or_default();
board.get("layers").and_then(|l| l.get("layers")).and_then(|a| a.as_array()).map(|layers| {
layers.iter().filter_map(|l| {
if str_of(l, "kind") != "mechanical" || l.get("displayed").and_then(|d| d.as_bool()) != Some(true) {
return None;
}
if occupied.contains(str_of(l, "id")) {
return None;
}
let name = str_of(l, "name");
Some(finding("unused_mechanical_layers", "info", ("layer", name),
format!("Mechanical layer {name:?} is enabled but carries no primitives.")))
}).collect()
}).unwrap_or_default()
}
/// info — a placed component whose origin is off the placement grid (5 mil default).
fn chk_off_grid_components(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
const GRID_MIL: f64 = 5.0;
components(board).iter().filter_map(|c| {
let x = c.get("xMil").and_then(|v| v.as_f64())?;
let y = c.get("yMil").and_then(|v| v.as_f64())?;
if (x.round() as i64) % (GRID_MIL as i64) != 0 || (y.round() as i64) % (GRID_MIL as i64) != 0 {
Some(finding("off_grid_components", "info", ("designator", str_of(c, "designator")),
format!("{} sits at ({x}, {y}) mil — off the {GRID_MIL} mil grid.", str_of(c, "designator"))))
} else { None }
}).collect()
}
/// error — the same designator twice on one sheet/board.
fn chk_duplicate_designators(board: &Value, sheet: Option<&Value>) -> Vec<Value> {
let mut out = Vec::new();
let sources: Vec<(&Value, &str)> = sheet.map(|sv| (sv, "sheet")).into_iter()
.chain(std::iter::once((board, "board")))
.collect();
for (src, label) in sources {
if src.is_null() {
continue;
}
let mut seen: std::collections::BTreeMap<String, u32> = std::collections::BTreeMap::new();
for c in components(src) {
let d = str_of(&c, "designator").to_string();
if !d.is_empty() {
*seen.entry(d).or_default() += 1;
}
}
for (d, n) in seen {
if n > 1 {
out.push(finding("duplicate_designators", "error", ("designator", &d),
format!("Designator {d} occurs {n} times on the {label}.")));
}
}
}
out
}
/// The board extent, or None. Only source == "boardOutline" is trustworthy for off-board claims.
fn outline_box(board: &Value) -> Option<(f64, f64, f64, f64)> {
let o = board.get("outline")?;
if o.get("ok").and_then(|b| b.as_bool()) != Some(true) || str_of(o, "source") != "boardOutline" {
return None;
}
Some((
o.get("leftMil")?.as_f64()?,
o.get("rightMil")?.as_f64()?,
o.get("bottomMil")?.as_f64()?,
o.get("topMil")?.as_f64()?,
))
}
/// error — a placed component outside the board extent (a BOUNDING BOX test, and it says so).
fn chk_components_outside_board(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
let Some((l, r, b, t)) = outline_box(board) else { return Vec::new() };
components(board).iter().filter_map(|c| {
let x = c.get("xMil").and_then(|v| v.as_f64())?;
let y = c.get("yMil").and_then(|v| v.as_f64())?;
if x < l || x > r || y < b || y > t {
Some(finding("components_outside_board_outline", "error", ("designator", str_of(c, "designator")),
format!("{} sits at ({x}, {y}) mil, outside the board bounding box ({l}..{r}, {b}..{t} mil).", str_of(c, "designator"))))
} else { None }
}).collect()
}
/// warning — a component ORIGIN within 20 mil of the board edge (origins, not pads — named for
/// what it measures).
fn chk_components_near_board_edge(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
const MARGIN_MIL: f64 = 20.0;
let Some((l, r, b, t)) = outline_box(board) else { return Vec::new() };
components(board).iter().filter_map(|c| {
let x = c.get("xMil").and_then(|v| v.as_f64())?;
let y = c.get("yMil").and_then(|v| v.as_f64())?;
if x < l || x > r || y < b || y > t {
return None; // already reported as outside
}
let d = (x - l).min(r - x).min(y - b).min(t - y);
if d < MARGIN_MIL {
Some(finding("components_near_board_edge", "warning", ("designator", str_of(c, "designator")),
format!("{} origin is {} mil from the board edge (under the {MARGIN_MIL} mil margin).", str_of(c, "designator"), d as i64)))
} else { None }
}).collect()
}
/// warning — a purchased component missing Mouser / DigiKey / LCSC part numbers (Mouser first:
/// it is the preferred vendor). Test points/fiducials/mounting holes are exempt.
fn chk_vendor_part_numbers(_board: &Value, sheet: Option<&Value>) -> Vec<Value> {
let Some(sheet) = sheet else { return Vec::new() };
components(sheet).iter().filter_map(|c| {
let des = str_of(c, "designator");
if !is_purchased(des) {
return None;
}
let blob = params_of(c).iter().map(|(k, v)| format!("{k} {v}")).collect::<Vec<_>>().join(" ");
let missing: Vec<&str> = VENDOR_PATTERNS.iter()
.filter(|(_, keys)| !keys.iter().any(|k| blob.contains(k)))
.map(|(vendor, _)| *vendor)
.collect();
if missing.is_empty() {
return None;
}
let mut f = finding("vendor_part_numbers", "warning", ("designator", des),
format!("{des} has no {} part number (house rule: Mouser preferred, all three required).", missing.join(" / ")));
f["missing"] = json!(missing);
Some(f)
}).collect()
}
/// info — a purchased component with no datasheet link.
fn chk_missing_datasheets(_board: &Value, sheet: Option<&Value>) -> Vec<Value> {
let Some(sheet) = sheet else { return Vec::new() };
components(sheet).iter().filter_map(|c| {
let des = str_of(c, "designator");
if !is_purchased(des) {
return None;
}
let has = params_of(c).iter().any(|(k, _)| {
matches!(k.as_str(), "datasheet" | "helpurl" | "datasheet link" | "datasheeturl") || k.contains("datasheet")
});
if has {
None
} else {
Some(finding("missing_datasheets", "info", ("designator", des),
format!("{des} has no Datasheet parameter.")))
}
}).collect()
}
/// error — a schematic component still carrying default text in Comment (caught at the source,
/// where the BOM is actually generated from).
fn chk_schematic_placeholders(_board: &Value, sheet: Option<&Value>) -> Vec<Value> {
let Some(sheet) = sheet else { return Vec::new() };
components(sheet).iter().filter_map(|c| {
let com = str_of(c, "comment").trim().to_string();
if PLACEHOLDER_VALUES.contains(&com.to_lowercase().as_str()) {
Some(finding("schematic_placeholder_values", "error", ("designator", str_of(c, "designator")),
format!("{} on the sheet has placeholder Comment {com:?}.", str_of(c, "designator"))))
} else { None }
}).collect()
}
fn rules(board: &Value) -> Vec<Value> {
board.get("rules").and_then(|r| r.get("rules")).and_then(|a| a.as_array()).cloned().unwrap_or_default()
}
/// warning/error — a design rule that exists but is switched OFF. A disabled Clearance or
/// ShortCircuit rule is worse than a missing one: the board LOOKS ruled and DRC passes.
fn chk_disabled_design_rules(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
rules(board).iter().filter_map(|r| {
if r.get("enabled").and_then(|e| e.as_bool()) != Some(false) {
return None;
}
let kind = r.get("kind").and_then(|k| k.as_i64()).unwrap_or(-1);
let kind_name = crate::board::rule_kind_name(kind);
let sev = if CRITICAL_RULE_KINDS.iter().any(|(k, _)| *k == kind) { "error" } else { "warning" };
let name = str_of(r, "name");
Some(finding("disabled_design_rules", sev, ("rule", name),
format!("Design rule {name:?} ({kind_name}) is DISABLED — DRC will pass without ever applying it.")))
}).collect()
}
/// error — no ENABLED rule of a kind a fabricator relies on.
fn chk_missing_critical_rules(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
let all = rules(board);
if all.is_empty() {
return Vec::new(); // no rule data at all is not the same as no rules
}
let have: std::collections::BTreeSet<i64> = all.iter()
.filter(|r| r.get("enabled").and_then(|e| e.as_bool()) != Some(false))
.filter_map(|r| r.get("kind").and_then(|k| k.as_i64()))
.collect();
CRITICAL_RULE_KINDS.iter().filter_map(|(kind, name)| {
if have.contains(kind) {
None
} else {
Some(finding("missing_critical_rules", "error", ("rule", name),
format!("No enabled {name} rule on this board.")))
}
}).collect()
}
/// info — two enabled rules of the same kind sharing a priority (Altium resolves ties by
/// internal order, which is not readable off the board).
fn chk_duplicate_rule_priority(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
let mut seen: std::collections::BTreeMap<(String, String), Vec<String>> = std::collections::BTreeMap::new();
for r in rules(board) {
if r.get("enabled").and_then(|e| e.as_bool()) == Some(false) {
continue;
}
let key = (r.get("kind").map(|k| k.to_string()).unwrap_or_default(),
r.get("priority").map(|p| p.to_string()).unwrap_or_default());
seen.entry(key).or_default().push(str_of(&r, "name").to_string());
}
seen.into_iter().filter_map(|((kind, prio), mut names)| {
if names.len() < 2 {
return None;
}
names.sort();
let kind_name = kind.parse::<i64>().map(crate::board::rule_kind_name).unwrap_or(kind.clone());
Some(finding("duplicate_rule_priority", "info", ("rule", &names.join(", ")),
format!("{} enabled {kind_name} rules share priority {prio} ({}) — which one wins is not readable from the board.", names.len(), names.join(", "))))
}).collect()
}
/// warning — a placed component whose footprint PATTERN is empty (no library identity).
fn chk_components_without_footprint(board: &Value, _sheet: Option<&Value>) -> Vec<Value> {
components(board).iter().filter_map(|c| {
if str_of(c, "pattern").trim().is_empty() {
Some(finding("components_without_footprint", "warning", ("designator", str_of(c, "designator")),
format!("{} has no footprint pattern name — it cannot be matched back to a library.", str_of(c, "designator"))))
} else { None }
}).collect()
}
// (id, fn). The ID is what appears in a finding's `check` field, so `checks:["..."]` filters on
// the name the caller actually sees — deriving it from a function name was a live trap.
type Check = fn(&Value, Option<&Value>) -> Vec<Value>;
pub const BOARD_CHECKS: [(&str, Check); 14] = [
("placeholder_values", chk_placeholder_values),
("single_pin_nets", chk_single_pin_nets),
("unused_mechanical_layers", chk_unused_mechanical_layers),
("off_grid_components", chk_off_grid_components),
("duplicate_designators", chk_duplicate_designators),
("components_outside_board_outline", chk_components_outside_board),
("components_near_board_edge", chk_components_near_board_edge),
// schematic-side: need includeSchematic:true, and quietly return nothing without it
("vendor_part_numbers", chk_vendor_part_numbers),
("missing_datasheets", chk_missing_datasheets),
("schematic_placeholder_values", chk_schematic_placeholders),
// design-rule hygiene, from the rules the board read already carries
("disabled_design_rules", chk_disabled_design_rules),
("missing_critical_rules", chk_missing_critical_rules),
("duplicate_rule_priority", chk_duplicate_rule_priority),
("components_without_footprint", chk_components_without_footprint),
];
const SHEET_CHECKS: [&str; 4] = ["vendor_part_numbers", "missing_datasheets",
"schematic_placeholder_values", "duplicate_designators"];
/// Run the checks over already-read data. Pure — this is what the fixture tests exercise.
pub fn run_checks(board: &Value, sheet: Option<&Value>, wanted: Option<&Vec<String>>) -> (Vec<Value>, Vec<String>, Value) {
let have_board = board.is_object() && !board.as_object().map(|o| o.is_empty()).unwrap_or(true);
let mut findings = Vec::new();
let mut ran = Vec::new();
let mut skipped = serde_json::Map::new();
for (name, f) in BOARD_CHECKS.iter() {
if let Some(w) = wanted {
if !w.iter().any(|x| x == name) {
continue;
}
}
let needs_sheet = SHEET_CHECKS.contains(name);
if !have_board && !needs_sheet {
skipped.insert(name.to_string(), json!("no board read"));
continue;
}
if needs_sheet && sheet.is_none() && *name != "duplicate_designators" {
skipped.insert(name.to_string(), json!("no schematic read — pass includeSchematic:true or sheet:<path>"));
continue;
}
ran.push(name.to_string());
findings.extend(f(board, sheet));
}
let sev_order = |s: &str| match s { "error" => 0, "warning" => 1, "info" => 2, _ => 9 };
findings.sort_by(|a, b| {
let sa = sev_order(a.get("severity").and_then(|s| s.as_str()).unwrap_or(""));
let sb = sev_order(b.get("severity").and_then(|s| s.as_str()).unwrap_or(""));
sa.cmp(&sb)
.then_with(|| str_of(a, "check").cmp(str_of(b, "check")))
.then_with(|| {
let ka = [str_of(a, "designator"), str_of(a, "net"), str_of(a, "layer")].concat();
let kb = [str_of(b, "designator"), str_of(b, "net"), str_of(b, "layer")].concat();
natural_cmp(&ka, &kb)
})
});
(findings, ran, Value::Object(skipped))
}
/// The verb: one agent round trip per document; all analysis in the bridge. Returns PARTIAL
/// results rather than nothing — a review that fails wholesale because one document was not
/// current is a review nobody runs.
pub fn review_board(args: &Value) -> Value {
if let Some(g) = support::need_licensed() {
return g;
}
let timeout = timeout_of(args, 90);
let open = |path: &str| -> Option<Value> {
if let Some(bad) = support::validate_openable(path) {
return Some(bad);
}
let o = agent::call("open", &[("path", path.to_string())], None, timeout);
let d = o.get("data").cloned().unwrap_or(json!({}));
if d.get("ok").and_then(|b| b.as_bool()) == Some(false) {
return Some(json!({"error": d.get("error")}));
}
None
};
let mut board_data = json!({});
let mut sheet_data: Option<Value> = None;
let mut board_err: Option<String> = None;
let mut sheet_err: Option<String> = None;
if let Some(bp) = crate::support::s(args, "board") {
if let Some(e) = open(&bp) {
let mut out = e;
out["success"] = json!(false);
out["stage"] = json!("board");
return out;
}
}
let r = agent::call("read_board",
&[("sections", "components,nets,layers,copper,rules,occupancy,outline".into()),
("limit", crate::support::s_or(args, "limit", "2000"))],
None, timeout);
let bd = r.get("data").cloned().unwrap_or(json!({}));
if bd.get("ok").and_then(|b| b.as_bool()) == Some(true) {
board_data = bd;
} else {
board_err = Some(bd.get("error").and_then(|e| e.as_str()).unwrap_or("board read failed").to_string());
}
let want_sheet = args.get("includeSchematic").and_then(|b| b.as_bool()) == Some(true) || args.get("sheet").is_some();
if want_sheet {
if let Some(sp) = crate::support::s(args, "sheet") {
if let Some(e) = open(&sp) {
sheet_err = e.get("error").and_then(|x| x.as_str()).map(str::to_string).or(Some("could not open sheet".into()));
}
}
if sheet_err.is_none() {
let sr = agent::call("read_schematic", &[("sections", "components".into()), ("params", "1".into())], None, timeout);
let sd = sr.get("data").cloned().unwrap_or(json!({}));
if sd.get("ok").and_then(|b| b.as_bool()) == Some(true) {
sheet_data = Some(sd);
} else {
sheet_err = Some(sd.get("error").and_then(|e| e.as_str()).unwrap_or("schematic read failed").to_string());
}
}
}
let have_board = board_err.is_none();
if !have_board && sheet_data.is_none() {
return json!({"success": false, "boardError": board_err, "schematicError": sheet_err,
"_hint": "Nothing could be read. Pass board:<path> and/or sheet:<path> — Altium holds only ONE document current, so the review activates each in turn."});
}
let wanted: Option<Vec<String>> = args.get("checks").and_then(|c| c.as_array())
.map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect());
let unknown: Vec<String> = wanted.as_ref().map(|w| {
w.iter().filter(|x| !BOARD_CHECKS.iter().any(|(n, _)| n == &x.as_str())).cloned().collect()
}).unwrap_or_default();
let board_for_checks = if have_board { board_data.clone() } else { json!({}) };
let (findings, ran, mut skipped) = run_checks(&board_for_checks, sheet_data.as_ref(), wanted.as_ref());
if let (Some(err), Some(obj)) = (&board_err, skipped.as_object_mut()) {
for (_, v) in obj.iter_mut() {
if v == &json!("no board read") {
*v = json!(format!("no board read ({err})"));
}
}
}
let mut counts: std::collections::BTreeMap<String, u64> = std::collections::BTreeMap::new();
for f in &findings {
*counts.entry(f.get("severity").and_then(|s| s.as_str()).unwrap_or("?").to_string()).or_default() += 1;
}
let mut out = json!({"success": true, "board": board_data.get("board"),
"sheet": sheet_data.as_ref().and_then(|s| s.get("document")),
"findings": findings, "counts": counts, "checksRun": ran,
"availableChecks": BOARD_CHECKS.iter().map(|(n, _)| *n).collect::<Vec<_>>(),
"notImplemented": NOT_IMPLEMENTED.iter().cloned().collect::<std::collections::BTreeMap<_, _>>(),
"_hint": "A clean result means the checks that RAN found nothing. `skipped` lists checks with no data; `notImplemented` lists what this review cannot see yet."});
if skipped.as_object().map(|o| !o.is_empty()).unwrap_or(false) {
out["skipped"] = skipped;
}
if !unknown.is_empty() {
out["unknownChecks"] = json!(unknown);
}
if let Some(e) = board_err {
out["boardError"] = json!(e);
}
if let Some(e) = sheet_err {
out["schematicError"] = json!(e);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_board() -> Value {
let raw = std::fs::read_to_string("fixtures/read_board.json").expect("fixture");
let v: Value = serde_json::from_str(&raw).expect("fixture json");
v.get("raw").cloned().unwrap_or(v)
}
#[test]
fn checks_run_clean_over_the_recorded_board() {
let board = fixture_board();
let (findings, ran, skipped) = run_checks(&board, None, None);
// board-side checks all ran; sheet-side were skipped (no sheet given)
assert!(ran.iter().any(|r| r == "placeholder_values"));
assert!(skipped.get("vendor_part_numbers").is_some());
// findings on a real board are plausible, but every one must carry the visible fields
for f in &findings {
assert!(f.get("check").is_some() && f.get("severity").is_some() && f.get("message").is_some());
}
}
#[test]
fn placeholder_and_duplicate_detection() {
let board = json!({"components": {"components": [
{"designator": "R1", "comment": "Comment"},
{"designator": "R1", "comment": "10k"},
{"designator": "U2", "comment": "STM32"},
]}});
let (findings, _, _) = run_checks(&board, None, Some(&vec!["placeholder_values".into(), "duplicate_designators".into()]));
assert!(findings.iter().any(|f| f["check"] == json!("placeholder_values") && f["designator"] == json!("R1")));
assert!(findings.iter().any(|f| f["check"] == json!("duplicate_designators")));
// errors sort before warnings before info
let sevs: Vec<&str> = findings.iter().map(|f| f["severity"].as_str().unwrap()).collect();
let mut sorted = sevs.clone();
sorted.sort_by_key(|s| match *s { "error" => 0, "warning" => 1, _ => 2 });
assert_eq!(sevs, sorted);
}
#[test]
fn vendor_rule_needs_the_sheet_and_exempts_non_purchased() {
let sheet = json!({"components": {"components": [
{"designator": "U1", "params": [{"name": "Mouser Part #", "value": "123"}]},
{"designator": "TP1", "params": []},
]}});
let (findings, _, _) = run_checks(&json!({}), Some(&sheet), Some(&vec!["vendor_part_numbers".into()]));
// U1 misses DigiKey + LCSC; TP1 is exempt
assert_eq!(findings.len(), 1);
assert_eq!(findings[0]["designator"], json!("U1"));
let missing = findings[0]["missing"].as_array().unwrap();
assert_eq!(missing.len(), 2);
// Mouser preferred: the message names what is MISSING, and Mouser is present here
assert!(!findings[0]["message"].as_str().unwrap().contains("no Mouser"));
}
#[test]
fn outline_bounding_box_checks_refuse_weak_sources() {
let board = json!({
"outline": {"ok": true, "source": "boardBounds", "leftMil": 0, "rightMil": 1000, "bottomMil": 0, "topMil": 1000},
"components": {"components": [{"designator": "U9", "xMil": 5000, "yMil": 5000}]}
});
// boardBounds is everything on the board incl. strays — a much weaker claim; no findings
let (findings, _, _) = run_checks(&board, None, Some(&vec!["components_outside_board_outline".into()]));
assert!(findings.is_empty());
let board = json!({
"outline": {"ok": true, "source": "boardOutline", "leftMil": 0, "rightMil": 1000, "bottomMil": 0, "topMil": 1000},
"components": {"components": [{"designator": "U9", "xMil": 5000, "yMil": 5000},
{"designator": "U8", "xMil": 995, "yMil": 500}]}
});
let (findings, _, _) = run_checks(&board, None, None);
assert!(findings.iter().any(|f| f["check"] == json!("components_outside_board_outline") && f["designator"] == json!("U9")));
assert!(findings.iter().any(|f| f["check"] == json!("components_near_board_edge") && f["designator"] == json!("U8")));
}
#[test]
fn rule_hygiene_checks() {
let board = json!({"components": {"components": []}, "rules": {"rules": [
{"name": "Clearance", "kind": 0, "priority": 1, "enabled": false},
{"name": "Width1", "kind": 2, "priority": 1, "enabled": true},
{"name": "Width2", "kind": 2, "priority": 1, "enabled": true},
]}});
let (findings, _, _) = run_checks(&board, None, None);
// disabled critical rule = error; missing HoleSize + ShortCircuit + Clearance(enabled) = errors
assert!(findings.iter().any(|f| f["check"] == json!("disabled_design_rules") && f["severity"] == json!("error")));
let missing: Vec<&str> = findings.iter().filter(|f| f["check"] == json!("missing_critical_rules"))
.map(|f| f["rule"].as_str().unwrap()).collect();
assert!(missing.contains(&"Clearance") && missing.contains(&"HoleSize") && missing.contains(&"ShortCircuit"));
assert!(findings.iter().any(|f| f["check"] == json!("duplicate_rule_priority")));
}
}