← Commit history
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.2.36"+version = "0.2.37" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.2.36"+version = "0.2.37" dependencies = [  "aiflow-board",  "serde",@@ -35,7 +35,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.2.36"+version = "0.2.37" dependencies = [  "roxmltree",  "serde",@@ -44,7 +44,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.2.36"+version = "0.2.37" dependencies = [  "aiflow-board",  "serde",@@ -53,7 +53,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.2.36"+version = "0.2.37" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.2.36"+version = "0.2.37" dependencies = [  "aiflow-board",  "serde",@@ -72,7 +72,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.2.36"+version = "0.2.37" dependencies = [  "aiflow-board",  "serde",@@ -81,7 +81,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.2.36"+version = "0.2.37" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.2.36"+version = "0.2.37" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.2.36"+version = "0.2.37" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.2.36"+version = "0.2.37" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/led_states.rs+81−12
@@ -38,6 +38,24 @@ pub fn leds(b: &Board) -> Vec<String> { /// `led-states.json` beside the board file. pub fn path_for(board: &str) -> PathBuf { Path::new(board).parent().unwrap_or(Path::new(".")).join(FILE) } +/// The entries `led-states.json` beside the board declares (empty when there is no readable file).+fn declared(board_path: &str) -> Vec<Value> {+    std::fs::read_to_string(path_for(board_path)).ok().and_then(|t| serde_json::from_str::<Value>(&t).ok())+        .and_then(|v| v["leds"].as_array().cloned()).unwrap_or_default()+}++/// The LEDs to describe: the ones the names give away (`leds`), plus every reference+/// `led-states.json` declares that is on the board (issue #122: an RGB part on a project footprint+/// named by its MPN has no "LED" anywhere in its name, and the file saying it is one is enough).+pub fn leds_on(board_path: &str, b: &Board) -> Vec<String> {+    let mut v = leds(b);+    for e in declared(board_path) {+        if let Some(r) = e["ref"].as_str() { if b.footprints.contains_key(r) && !v.iter().any(|x| x == r) { v.push(r.to_string()); } }+    }+    v.sort();+    v+}+ fn s<'a>(v: &'a Value, k: &str) -> Option<&'a str> { v.get(k).and_then(|x| x.as_str()).filter(|x| !x.trim().is_empty()) }  /// What is wrong with one entry (empty when it is complete), checked against the LED's own pads.@@ -76,26 +94,40 @@ fn entry_problems(e: &Value, b: &Board) -> Vec<String> { /// Check the board's `led-states.json`: Ok(None) when the board has no LED, Ok(Some(path)) when every /// LED has a complete entry, Err(problems) otherwise. pub fn check(board_path: &str, b: &Board) -> Result<Option<PathBuf>, Vec<String>> {-    let want = leds(b);-    if want.is_empty() { return Ok(None); }+    let want = leds_on(board_path, b);+    let ghosts: Vec<String> = declared(board_path).iter().filter_map(|e| e["ref"].as_str()).filter(|r| !b.footprints.contains_key(*r)).map(|r| format!("{r} is in {FILE} but not on the board")).collect();+    if want.is_empty() && ghosts.is_empty() { return Ok(None); }     let path = path_for(board_path);     let Ok(text) = std::fs::read_to_string(&path) else { return Err(vec![format!("no {} beside the board for {}", FILE, want.join(", "))]) };     let v: Value = serde_json::from_str(&text).map_err(|e| vec![format!("{} is not JSON: {e}", path.display())])?;-    let mut problems = Vec::new();+    let mut problems = ghosts;     if v["$schema"].as_str() != Some(SCHEMA) { problems.push(format!("$schema must be {SCHEMA:?}")); }     let entries = v["leds"].as_array().cloned().unwrap_or_default();     for r in &want {-        match entries.iter().find(|e| e["ref"].as_str() == Some(r.as_str())) {-            None => problems.push(format!("{r} has no entry")),-            Some(e) => { let p = entry_problems(e, b); if !p.is_empty() { problems.push(format!("{r} lacks {}", p.join(", "))); } }+        // every record of a part counts: an RGB package has one per die (issue #122)+        let mine: Vec<&Value> = entries.iter().filter(|e| e["ref"].as_str() == Some(r.as_str())).collect();+        if mine.is_empty() { problems.push(format!("{r} has no entry")); continue; }+        if mine.len() > 1 {+            let mut seen: Vec<&str> = Vec::new();+            for e in &mine {+                match s(e, "channel") {+                    None => { problems.push(format!("{r} has {} entries: give each die its own \"channel\" (e.g. red, green, blue)", mine.len())); break; }+                    Some(c) if seen.contains(&c) => problems.push(format!("{r} lists channel {c:?} twice")),+                    Some(c) => seen.push(c),+                }+            }+        }+        for e in &mine {+            let p = entry_problems(e, b);+            if !p.is_empty() { problems.push(format!("{r}{} lacks {}", s(e, "channel").map(|c| format!(" ({c})")).unwrap_or_default(), p.join(", "))); }         }     }     if problems.is_empty() { Ok(Some(path)) } else { Err(problems) } }  /// The finish line of the LED-state check, printed on a pass too (issue #97).-pub fn summary(b: &Board, result: &Result<Option<PathBuf>, Vec<String>>) -> String {-    let want = leds(b);+pub fn summary(board_path: &str, b: &Board, result: &Result<Option<PathBuf>, Vec<String>>) -> String {+    let want = leds_on(board_path, b);     match result {         Ok(None) => "LED states: no LEDs on the board: led-states.json not required".into(),         Ok(Some(p)) => format!("LED states: {} LED(s) ({}), each described in {}", want.len(), want.join(", "), p.display()),@@ -110,7 +142,7 @@ pub fn gate_message(problems: &[String]) -> String {  /// The hint for a board with LEDs (schematic, silkscreen and finish stages). pub fn hint(board_path: &str, b: &Board) -> Option<String> {-    let want = leds(b);+    let want = leds_on(board_path, b);     if want.is_empty() { return None; }     let state = match check(board_path, b) { Ok(_) => "complete".to_string(), Err(p) => format!("open: {}", p.join("; ")) };     Some(format!("LEDs on this board: {}. Describe each one's on/off state in {} beside the board ({state}): {{\"$schema\":\"{SCHEMA}\",\"leds\":[{{\"ref\",\"mpn\",\"colour\":{{\"name\",\"dominant_nm\",\"hex\"}},\"anode_net\",\"cathode_net\",\"series\":[\"R1\"],\"current_mA\" (from the rail, Vf and the series resistor),\"on_when\":{{\"kind\":\"rail\",\"net\",\"min_v\"}} | {{\"kind\":\"gpio\",\"mcu\",\"pin\",\"port\",\"active\":\"high|low\"}} | {{\"kind\":\"signal\",\"net\",\"active\"}},\"evidence\"}}]}} and list it in board-release.json under ledStates. `finish` refuses the board until every LED has an entry. For each LED part, add led.json ({{\"$schema\":\"{PART_SCHEMA}\",\"colour\":{{name,dominant_nm,hex}},\"vf_V\":{{typ,max,at_mA}},\"if_max_mA\",\"viewing_deg\",\"glb_lens_material\"}}) to its wiki component page when you may edit that page; otherwise open an issue on the page carrying that record. Never edit someone else's page.", want.join(", "), FILE))@@ -176,15 +208,52 @@ mod tests {         std::fs::remove_dir_all(&dir).ok();     } +    #[test]+    fn a_declared_rgb_part_is_checked_die_by_die() {+        // issue #122: D1 on a project footprint named by its MPN, common anode, three cathodes+        let dir = std::env::temp_dir().join(format!("aiflow-rgb-{}", std::process::id()));+        std::fs::create_dir_all(&dir).unwrap();+        let bp = dir.join("b.kicad_pcb").display().to_string();+        let b = Board::from_text(&bp, r#"(kicad_pcb (version 20241229)+  (net 0 "") (net 1 "VDD") (net 2 "R_K") (net 3 "G_K") (net 4 "B_K")+  (gr_rect (start 0 0) (end 30 20) (layer "Edge.Cuts") (width 0.1))+  (footprint "Project:RGB4" (layer "F.Cu") (at 10 10)+    (property "Reference" "D1" (at 0 -2) (layer "F.SilkS"))+    (pad "1" smd rect (at -1 0) (size 0.6 0.6) (layers "F.Cu") (net 1 "VDD")) (pad "2" smd rect (at 0 0) (size 0.6 0.6) (layers "F.Cu") (net 2 "R_K"))+    (pad "3" smd rect (at 1 0) (size 0.6 0.6) (layers "F.Cu") (net 3 "G_K")) (pad "4" smd rect (at 2 0) (size 0.6 0.6) (layers "F.Cu") (net 4 "B_K")))+)"#.to_string()).unwrap();+        assert!(leds(&b).is_empty(), "the name gives nothing away");+        let die = |c: &str, k: &str| json!({"ref": "D1", "channel": c, "colour": {"name": c}, "anode_net": "VDD", "cathode_net": k, "current_mA": 5, "on_when": {"kind": "gpio", "mcu": "U1", "pin": "4", "active": "low"}});+        let write = |leds: Vec<Value>| std::fs::write(dir.join(FILE), json!({"$schema": SCHEMA, "leds": leds}).to_string()).unwrap();+        write(vec![die("red", "R_K"), die("green", "G_K"), die("blue", "B_K")]);+        assert_eq!(leds_on(&bp, &b), vec!["D1"]);+        assert_eq!(check(&bp, &b).unwrap(), Some(dir.join(FILE)));+        assert!(summary(&bp, &b, &check(&bp, &b)).starts_with("LED states: 1 LED(s) (D1), each described"));+        // every die is checked: a wrong cathode on the third one fails+        write(vec![die("red", "R_K"), die("green", "G_K"), die("blue", "NOPE")]);+        assert!(check(&bp, &b).unwrap_err().iter().any(|p| p.contains("D1 (blue) lacks cathode_net \"NOPE\"")));+        // duplicate channels and unnamed dies fail+        write(vec![die("red", "R_K"), die("red", "G_K")]);+        assert!(check(&bp, &b).unwrap_err().iter().any(|p| p.contains("channel \"red\" twice")));+        let mut unnamed = die("red", "R_K"); unnamed.as_object_mut().unwrap().remove("channel");+        write(vec![unnamed, die("green", "G_K")]);+        assert!(check(&bp, &b).unwrap_err().iter().any(|p| p.contains("its own \"channel\"")));+        // a declared reference that is not on the board fails+        let mut ghost = die("red", "R_K"); ghost["ref"] = json!("D9");+        write(vec![die("red", "R_K"), die("green", "G_K"), die("blue", "B_K"), ghost]);+        assert!(check(&bp, &b).unwrap_err().iter().any(|p| p.contains("D9 is in led-states.json but not on the board")));+        std::fs::remove_dir_all(&dir).ok();+    }+     #[test]     fn the_led_check_says_what_it_found_on_a_pass() {         // issue #97         let b = Board::from_text("/nonexistent/b.kicad_pcb", "(kicad_pcb (version 20241229)\n  (gr_rect (start 0 0) (end 10 10) (layer \"Edge.Cuts\") (width 0.1))\n)".to_string()).unwrap();         let r = check("/nonexistent/b.kicad_pcb", &b);-        assert_eq!(summary(&b, &r), "LED states: no LEDs on the board: led-states.json not required");+        assert_eq!(summary("/nonexistent/b.kicad_pcb", &b, &r), "LED states: no LEDs on the board: led-states.json not required");         let b = Board::from_text("/nonexistent/b.kicad_pcb", BOARD.to_string()).unwrap();         let r = check("/nonexistent/b.kicad_pcb", &b);-        assert!(summary(&b, &r).starts_with("LED states: 1 LED(s) (D1), 1 problem(s)"), "{}", summary(&b, &r));-        assert!(summary(&b, &Ok(Some(PathBuf::from("x/led-states.json")))).contains("each described in x/led-states.json"));+        assert!(summary("/nonexistent/b.kicad_pcb", &b, &r).starts_with("LED states: 1 LED(s) (D1), 1 problem(s)"), "{}", summary("/nonexistent/b.kicad_pcb", &b, &r));+        assert!(summary("/nonexistent/b.kicad_pcb", &b, &Ok(Some(PathBuf::from("x/led-states.json")))).contains("each described in x/led-states.json"));     } }
crates/adom-aiflow/src/main.rs+1−1
@@ -4407,7 +4407,7 @@ fn main() {             let mut check_lines = silk_polarity::label_check_lines(&label_checks);             // 0.2.18: every LED's on/off state, and a programmable part's interface where the workcell reaches it             let led_check = led_states::check(&solved_board, &board_now());-            check_lines.push(led_states::summary(&board_now(), &led_check));+            check_lines.push(led_states::summary(&solved_board, &board_now(), &led_check));             let led_file = match led_check { Ok(p) => p, Err(p) => { missing.push(led_states::gate_message(&p)); None } };             let prog_check = programming::check(&solved_board, &board_now(), &spec_soft(&r));             check_lines.push(programming::summary(&solved_board, &board_now(), &spec_soft(&r), &prog_check));
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.36",+  "version": "0.2.37",   "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.36",+  "version": "0.2.37",   "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.",
skills/adom-aiflow/SKILL.md+2
@@ -648,6 +648,8 @@ Every LED on the board is described in `led-states.json` beside the board, and l   "evidence": "<datasheet page / calc>"}]} ``` +An LED is any reference AI Flow recognises (LED*, or D*/CR* on an LED footprint) and any reference this file declares, so an RGB part on a project footprint named by its MPN is covered by listing it. A multi-die part (RGB) takes one entry per die with the same `ref` and a distinct `channel` (`red`, `green`, `blue`); each die's anode and cathode nets are checked against that part's own pads. A declared reference that is not on the board fails.+ `on_when.kind` is `rail` (`net`, `min_v`: lit whenever that rail is up, a power LED), `gpio` (`mcu`, `pin`, `port`, `active`: `high` or `low`, firmware-driven) or `signal` (`net`, `active`: driven by another circuit). `current_mA` is computed from the rail, the LED's Vf and the series resistor. `anode_net` and `cathode_net` must be nets on the LED's own pads. `step schematic`, `step silkscreen` and `step finish` print the Hint; `finish` refuses the board while any LED has no complete entry.  `silkscreen-audit --record` and `finish` print one line per label check on every run, a pass included (`power inputs: 0 found`, `power inputs: 1 (J1 VIN), all labelled with a voltage`, `test points: ...`), and record `powerInputs` and `testPoints` (`found`, `labelled`, `unlabelled`, `mismatched`, `items`) in the audit's result JSON, the `silkscreen` outcome and the `finish-checks` outcome. `finish` also prints `LED states: no LEDs on the board: led-states.json not required` (or what it found) and `programming: no programmable part: programming not required` (or the parts and interface).