← Commit history

Publish 0.2.34

John Lauer ·1e5a24f1f7 · ·parent d057c93
6 files changed +108−13
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.2.33"+version = "0.2.34" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.2.33"+version = "0.2.34" dependencies = [  "aiflow-board",  "serde",@@ -35,7 +35,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.2.33"+version = "0.2.34" dependencies = [  "roxmltree",  "serde",@@ -44,7 +44,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.2.33"+version = "0.2.34" dependencies = [  "aiflow-board",  "serde",@@ -53,7 +53,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.2.33"+version = "0.2.34" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.2.33"+version = "0.2.34" dependencies = [  "aiflow-board",  "serde",@@ -72,7 +72,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.2.33"+version = "0.2.34" dependencies = [  "aiflow-board",  "serde",@@ -81,7 +81,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.2.33"+version = "0.2.34" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.2.33"+version = "0.2.34" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.2.33"+version = "0.2.34" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.2.33"+version = "0.2.34" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/checks.rs+95
@@ -667,6 +667,61 @@ fn is_fab(base: &str) -> bool { base == "fab" || base == "adom-3rdpartyfab" }  /// The `(slug, layers)` a `rules use` pack declares in its header /// (`# adom-aiflow rules: <slug> <n>L (from ...)`), or the shipped jlcpcb-2L marker.+/// KiCad's custom-rule constraint types (KiCad 8 to 10). A file KiCad cannot parse is dropped whole+/// and silently: kicad-cli's DRC then runs on the board-setup defaults and reports no error (issue+/// #120, measured with KiCad 10.0.6: a file with an unknown `holex` constraint lost its track rule too).+/// `hole` is accepted (it judges drills like hole_size; same 10.0.6 measurement).+pub const DRU_CONSTRAINTS: &[&str] = &[+    "annular_width", "assertion", "bridged_mask", "clearance", "connection_width", "courtyard_clearance",+    "creepage", "diff_pair_gap", "diff_pair_uncoupled", "disallow", "edge_clearance", "hole", "hole_clearance",+    "hole_size", "hole_to_hole", "length", "min_resolved_spokes", "physical_clearance",+    "physical_hole_clearance", "silk_clearance", "skew", "solder_mask_expansion", "solder_paste_abs_margin",+    "solder_paste_rel_margin", "text_height", "text_thickness", "thermal_relief_gap", "thermal_spoke_width",+    "track_angle", "track_segment_length", "track_width", "via_count", "via_dangling", "via_diameter",+    "zone_connection",+];++/// Read a .kicad_dru the way KiCad's rules parser does, so a file KiCad would drop fails here+/// instead of passing on its name. Errors name the line's problem.+pub fn dru_syntax(text: &str) -> Result<usize, String> {+    // `#` starts a comment to the end of the line (outside quotes)+    let mut clean = String::with_capacity(text.len());+    for line in text.lines() {+        let mut quoted = false;+        let mut cut = line.len();+        for (i, c) in line.char_indices() {+            if c == '"' { quoted = !quoted; }+            if c == '#' && !quoted { cut = i; break; }+        }+        clean.push_str(&line[..cut]);+        clean.push('\n');+    }+    let root = sx::parse(&format!("(dru {clean})")).map_err(|e| format!("not a valid rules file ({e})"))?;+    if root.items.iter().any(|i| matches!(i, sx::Item::Atom { .. })) { return Err("text outside a (...) list".into()); }+    let top: Vec<&sx::Node> = root.nodes().collect();+    if top.first().map(|n| n.tag.as_str()) != Some("version") { return Err("missing (version N) as the first statement".into()); }+    let mut rules = 0;+    for n in &top[1..] {+        if n.tag != "rule" { return Err(format!("unexpected ({}) at the top level: only (version) and (rule) belong there", n.tag)); }+        let name = n.atom(1).unwrap_or("?").to_string();+        let mut constraints = 0;+        for c in n.nodes() {+            match c.tag.as_str() {+                "constraint" => {+                    let kind = c.atom(1).unwrap_or("");+                    if !DRU_CONSTRAINTS.contains(&kind) { return Err(format!("rule \"{name}\": unknown constraint type `{kind}` (KiCad drops the whole file, so no rule in it applies); KiCad's types include hole_size, hole_clearance, hole_to_hole, annular_width, track_width, clearance")); }+                    constraints += 1;+                }+                "condition" | "layer" | "severity" => {}+                other => return Err(format!("rule \"{name}\": unexpected ({other}); a rule holds constraint, condition, layer and severity")),+            }+        }+        if constraints == 0 { return Err(format!("rule \"{name}\" has no constraint")); }+        rules += 1;+    }+    Ok(rules)+}+ pub fn pack_of(text: &str) -> Option<(String, Option<u32>)> {     for line in text.lines() {         if let Some(rest) = line.split("adom-aiflow rules: ").nth(1) {@@ -685,6 +740,11 @@ pub fn pack_of(text: &str) -> Option<(String, Option<u32>)> { pub fn rules_check_layers(profile: &str, board: &str, fallback_layers: Option<u32>) -> Result<String, String> {     let dru = Path::new(board).with_extension("kicad_dru");     let text = std::fs::read_to_string(&dru).ok();+    if let Some(t) = &text {+        if let Err(e) = dru_syntax(t) {+            return Err(format!("{} cannot be read by KiCad: {e}. KiCad then silently ignores every rule in it, so DRC would pass on the defaults. Fix the file (or regenerate it); if your KiCad does accept this, `adom-aiflow deviate \"rules\" --why ...` and send AI Flow a PR adding the keyword", dru.display()));+        }+    }     let (base, _) = split_profile(profile);     let layers = effective_layers(profile, board, fallback_layers);     let want = |b: &str| match layers { Some(n) => format!("{b}-{n}L"), None => b.to_string() };@@ -1413,3 +1473,38 @@ mod tests {         assert!(!flat_chip("Capacitor_SMD:CP_Elec_6.3x5.8"));     } }++#[cfg(test)]+mod dru_syntax_tests {+    use super::*;++    #[test]+    fn kicad_readable_rules_pass_and_dropped_files_fail() {+        let good = "(version 1)\n# a comment\n(rule \"min track\" (layer outer)\n  (condition \"A.Type == 'track'\") (constraint track_width (min 0.15mm)) (severity error))\n(rule \"holes\" (constraint hole_size (min 0.3mm)) (constraint hole_to_hole (min 0.25mm)))";+        assert_eq!(dru_syntax(good), Ok(2));+        assert_eq!(dru_syntax(include_str!("../../../rules/jlcpcb-2L.kicad_dru")).map(|n| n > 0), Ok(true));+        // issue #120: KiCad 10.0.6 drops a file with an unknown constraint whole (`holex` lost the+        // track rule beside it); `hole` itself is accepted and judges drills like hole_size+        assert_eq!(dru_syntax("(version 1)\n(rule \"min hole\" (constraint hole (min 0.3mm)))"), Ok(1));+        let e = dru_syntax("(version 1)\n(rule \"min hole\" (constraint holex (min 0.3mm)))").unwrap_err();+        assert!(e.contains("unknown constraint type `holex`"), "{e}");+        assert!(dru_syntax("(rule \"x\" (constraint track_width (min 0.1mm)))").unwrap_err().contains("version"));+        assert!(dru_syntax("(version 1)\n(rule \"x\" (constraint track_width (min 0.1mm))").is_err());+        assert!(dru_syntax("(version 1)\n(rule \"x\" (condition \"A.Type == 'via'\"))").unwrap_err().contains("no constraint"));+        assert!(dru_syntax("(version 1)\n(rul \"x\" (constraint clearance (min 0.1mm)))").is_err());+        assert!(dru_syntax("(version 1)\n(rule \"x\" (constraint clearance (min 0.1mm)) (when x))").is_err());+    }++    #[test]+    fn the_fab_profile_no_longer_passes_on_the_word_rule() {+        let dir = std::env::temp_dir().join(format!("aiflow-dru-{}", std::process::id()));+        std::fs::create_dir_all(&dir).unwrap();+        let board = dir.join("b.kicad_pcb");+        std::fs::write(&board, "(kicad_pcb (version 20241229))").unwrap();+        std::fs::write(dir.join("b.kicad_dru"), "(version 1)\n(rule \"min hole\" (constraint holex (min 0.3mm)))").unwrap();+        assert!(rules_check_layers("fab", board.to_str().unwrap(), None).unwrap_err().contains("cannot be read by KiCad"));+        std::fs::write(dir.join("b.kicad_dru"), "(version 1)\n(rule \"min hole\" (constraint hole_size (min 0.3mm)))").unwrap();+        assert!(rules_check_layers("fab", board.to_str().unwrap(), None).is_ok());+        let _ = std::fs::remove_dir_all(&dir);+    }+}
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.33",+  "version": "0.2.34",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.33",+  "version": "0.2.34",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",