app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
← Commit history
Merge PR #443: 1.0.105: a warning type at KiCad's DRC cap no longer blocks live routing (closes #140)
3 files changed
+71−8
rust/Cargo.lock+3−3@@ -319,7 +319,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.104"+version = "1.0.105" dependencies = [ "adom-eda-checks", "kicad-core",@@ -331,7 +331,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.104"+version = "1.0.105" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -358,7 +358,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.104"+version = "1.0.105" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.104"+version = "1.0.105" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-core/src/ipc.rs+67−4@@ -241,13 +241,43 @@ fn fingerprint(v: &Value) -> String { json!([v.get("type"), v.get("severity"), v.get("description"), positions]).to_string() } +/// The capped violation types in a report that hold errors (wiki #140). KiCad caps each type on+/// its own and grades every row of a type alike, so a warning type at the cap can hide neither+/// a new error of another type nor one of its own. The grade is read from the rows here, not+/// when the limits were computed, so a caller that re-grades rows (the silkscreen check grades+/// every finding an error) keeps those types. A report that says it is truncated without naming+/// the types counts as truncated.+pub fn capped_error_types(r: &Value) -> Vec<String> {+ if !r.get("violationReportMayBeTruncated").and_then(Value::as_bool).unwrap_or(false) {+ return Vec::new();+ }+ let Some(types) = r.get("possiblyTruncatedViolationTypes").and_then(Value::as_array) else {+ return vec!["(unnamed)".into()];+ };+ let rows = r.get("violations").and_then(Value::as_array).map(Vec::as_slice).unwrap_or(&[]);+ let mut out: Vec<String> = types+ .iter()+ .filter(|t| rows.iter().any(|v| v.get("type") == Some(*t) && v.get("severity").and_then(Value::as_str) == Some("error")))+ .map(|t| t.as_str().map(str::to_string).unwrap_or_else(|| t.to_string()))+ .collect();+ out.sort();+ out.dedup();+ out+}+ /// `_new_errors`: the error-severity violations in `after` that `before` does not /// account for, one to one (an existing error never masks a second identical one).-/// Refuses to certify a delta when either list may be capped (`drc_incomplete`).+/// Refuses to certify a delta when either list may be capped in a type that holds errors+/// (`drc_incomplete`); warning types at the cap are left to violationReportMayBeTruncated. pub fn new_errors(before: &Value, after: &Value) -> RResult<Vec<Value>> {- let truncated = |r: &Value| r.get("violationReportMayBeTruncated").and_then(Value::as_bool).unwrap_or(false);- if truncated(before) || truncated(after) {- return Err(RoutingError::new("drc_incomplete", "DRC violation lists may be capped; cannot certify the error delta. No live copper was changed").with("mutated", json!(false)));+ let mut capped = capped_error_types(before);+ capped.extend(capped_error_types(after));+ capped.sort();+ capped.dedup();+ if !capped.is_empty() {+ return Err(RoutingError::new("drc_incomplete", format!("DRC lists of error types may be capped ({}); cannot certify the error delta. No live copper was changed", capped.join(", ")))+ .with("mutated", json!(false))+ .with("cappedErrorTypes", json!(capped))); } let errors = |r: &Value| -> Vec<Value> { r.get("violations").and_then(Value::as_array).map(|a| a.iter().filter(|v| v.get("severity").and_then(Value::as_str) == Some("error")).cloned().collect()).unwrap_or_default() }; let mut existing: Vec<(String, usize)> = Vec::new();@@ -2054,6 +2084,39 @@ mod tests { } } + /// Wiki #140: 199 silk_overlap and 199 silk_over_copper warnings on a dense LED board blocked+ /// every live route although the board had no errors.+ #[test]+ fn warning_types_at_the_cap_do_not_block_the_error_delta() {+ let warn = |kind: &str| json!({"type": kind, "severity": "warning", "description": "silk", "items": []});+ let mut rows: Vec<Value> = vec![warn("silk_overlap"); 199];+ rows.extend(vec![warn("silk_over_copper"); 199]);+ let mut report = json!({"violations": rows});+ let limits = report_limits(report["violations"].as_array().unwrap(), 499);+ assert_eq!(limits["violationReportMayBeTruncated"], true, "still reported for information");+ assert_eq!(limits["possiblyTruncatedViolationTypes"], json!(["silk_over_copper", "silk_overlap"]));+ merge(&mut report, &limits);+ assert!(capped_error_types(&report).is_empty());+ assert_eq!(new_errors(&report, &report).unwrap(), Vec::<Value>::new());+ // a new error of another type is still seen+ let mut after = report.clone();+ let short = json!({"type": "shorting_items", "severity": "error", "description": "short", "items": []});+ after["violations"].as_array_mut().unwrap().push(short.clone());+ assert_eq!(new_errors(&report, &after).unwrap(), vec![short]);+ // re-graded as errors (the silkscreen check does this), the same capped types block again+ let regraded = |mut v: Value| {+ for r in v["violations"].as_array_mut().unwrap() {+ r["severity"] = json!("error");+ }+ v+ };+ let e = new_errors(®raded(report.clone()), ®raded(report.clone())).unwrap_err();+ assert_eq!(e.code, "drc_incomplete");+ assert_eq!(e.detail["cappedErrorTypes"], json!(["silk_over_copper", "silk_overlap"]));+ // a report that is truncated without naming its types stays refused+ assert!(new_errors(&json!({"violations": [], "violationReportMayBeTruncated": true}), &json!({"violations": []})).is_err());+ }+ #[test] fn existing_error_does_not_mask_additional_same_error() { let v = json!({"type": "clearance", "severity": "error", "description": "collision", "items": [{"pos": {"x": 1, "y": 2}}]});