app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← Commit history
Silkscreen audit obstacles are checked against the board's own pads and vias, at record and at finish (0.1.97)
8 files changed
+91−14
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.1.96"+version = "0.1.97" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.1.96"+version = "0.1.97" dependencies = [ "aiflow-board", "serde",@@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.1.96"+version = "0.1.97" dependencies = [ "roxmltree", "serde",@@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.1.96"+version = "0.1.97" dependencies = [ "aiflow-board", "serde",@@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.1.96"+version = "0.1.97" dependencies = [ "aiflow-board", "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.1.96"+version = "0.1.97" dependencies = [ "aiflow-board", "serde",@@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.1.96"+version = "0.1.97" dependencies = [ "aiflow-board", "serde",@@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.1.96"+version = "0.1.97" dependencies = [ "aiflow-board", "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.1.96"+version = "0.1.97" dependencies = [ "aiflow-board", "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.1.96"+version = "0.1.97" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.1.96"+version = "0.1.97" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+1@@ -288,6 +288,7 @@ The gates read the board and the tools' own output, so a file written to look li - **3D models are the parts.** A renamed or resized laser-etch proxy box is refused by `models` and `finish`. Use the wiki component page's model, the vendor STEP, or a model built from the datasheet. - **Silkscreen is printed.** `finish` refuses a board with fewer than 0.5 visible silkscreen labels per footprint (finished boards carry about 1 to 3.5): references, values and contact labels go on the silkscreen. - `step <name> --back` only returns to an earlier step.+- **Silkscreen audit geometry is the board's (0.1.97).** `silkscreen-audit --record` and `finish` check the supplied obstacle boxes against the saved board: every pad of every footprint must fall inside one of that footprint's obstacle boxes (ids `U1`, `U1:body`, `U1:pad:1`), and every via inside a via/hole box as large as the via. Measure the boxes from the board; placeholder or point-sized boxes are refused. - **Thermal counts the board's vias (0.1.96).** `analyze thermal` counts the vias on each tab net in the saved board as well as the routing plan, so thermal vias landed after routing count once the board is saved. - **Budgets are fixed once placement begins (0.1.95).** The spec's budgets and physical models (`loads`, `hot` watts and `tabNet`, `hotMaxRiseC`/`maxRiseC`, `switchNodeMaxLengthMm`, `wideNets`/`midNets`, `fab` and copper weights, via/drill power, package cooling) freeze at placement in `<run>/spec-budgets.json`. When a check fails, change the board: more copper, vias, a tighter placement. Every gate refuses a spec whose budgets moved since. Layout keys (`pours`, `thermalVias`, fixed coordinates, `planeUnder`) stay editable. If a budget really is wrong (a misread datasheet value), `adom-aiflow spec-amend --why "<datasheet page and what was misread>"` records the change; amendments are listed in the run ledger and the finish summary.
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+8@@ -3433,6 +3433,14 @@ fn main() { if !sharp.is_empty() && r.data["sequence"].is_array() { missing.push(format!("outline: {} sharp corner(s) (outline round)", sharp.len())); } } for f in switchnode::findings(&spec_f, &board_now()) { missing.push(format!("switch node: {f} (back to placement)")); }+ // a recorded silkscreen proof is re-checked against the board's own pads and vias (the audit's geometry is supplied)+ if let Some(inp) = o.get("silkscreen").and_then(|p| p["input"].as_str()) {+ if let Ok(t) = std::fs::read_to_string(inp) {+ if let Ok(v) = serde_json::from_str::<Value>(&t) {+ for g in silkproof::geometry_gaps(&v, &board_now()) { missing.push(format!("silkscreen audit: {g} (silkscreen-audit --record again on real geometry)")); }+ }+ }+ } // the test plan again, on the finished board: every measurement still has a landing if let Some(tp) = r.data["testPlan"].as_str().map(str::to_string) { let list = testplan::tests_of(&read_json(&tp));
crates/adom-aiflow/src/silkproof.rs+69−1@@ -26,6 +26,7 @@ pub fn proof(audit: &Value, input: &Value, board: &Board) -> Result<Value, Vec<S let vias = board.root.find_all("via").count(); let via_items = inventory.iter().filter(|i| matches!(i["kind"].as_str(), Some("via" | "hole"))).count(); if via_items < vias { why.push(format!("the audit inventory has {via_items} via/hole item(s), the board {vias} vias")); }+ why.extend(geometry_gaps(input, board)); let (labels, fps) = crate::provenance::silk_labels(board); if fps > 0 && (labels as f64) < crate::provenance::MIN_SILK_PER_FOOTPRINT * fps as f64 { why.push(format!("{labels} visible silkscreen label(s) for {fps} footprints (at least {:.1} per footprint)", crate::provenance::MIN_SILK_PER_FOOTPRINT));@@ -39,6 +40,50 @@ pub fn proof(audit: &Value, input: &Value, board: &Board) -> Result<Value, Vec<S })) } +/// How far a supplied box may fall short of the board's own geometry (mm).+const TOL: f64 = 0.05;++fn boxes_for<'a>(obstacles: &'a [Value], f: impl Fn(&Value) -> bool + 'a) -> impl Iterator<Item = (f64, f64, f64, f64)> + 'a {+ obstacles.iter().filter(move |o| f(o)).filter_map(|o| {+ let b = o["box"].as_array()?;+ let v: Vec<f64> = b.iter().filter_map(|x| x.as_f64()).collect();+ (v.len() == 4).then(|| (v[0].min(v[2]), v[1].min(v[3]), v[0].max(v[2]), v[1].max(v[3])))+ })+}++fn inside(p: (f64, f64), b: (f64, f64, f64, f64)) -> bool {+ p.0 >= b.0 - TOL && p.0 <= b.2 + TOL && p.1 >= b.1 - TOL && p.1 <= b.3 + TOL+}++/// The supplied obstacle geometry checked against the board itself (2026-10-15: a run passed the audit+/// with every body a 0.4 mm square and every label a 0.6 mm square, so nothing could collide): every+/// pad corner must fall inside an obstacle box of its own footprint, and every via must sit inside a+/// via/hole box as large as the via.+pub fn geometry_gaps(input: &Value, board: &Board) -> Vec<String> {+ let obstacles = input["obstacles"].as_array().cloned().unwrap_or_default();+ let mut short: Vec<String> = Vec::new();+ for reference in board.footprints.keys() {+ let mine: Vec<_> = boxes_for(&obstacles, |o| o["id"].as_str().map(|id| covers(id, reference)).unwrap_or(false)).collect();+ let pads: Vec<_> = board.pads.iter().filter(|p| &p.reference == reference).collect();+ if pads.is_empty() { continue; }+ let missed = pads.iter().flat_map(|p| p.polys.iter().flatten().copied().collect::<Vec<_>>()).filter(|&pt| !mine.iter().any(|&b| inside(pt, b))).count();+ if missed > 0 { short.push(reference.clone()); }+ }+ let via_boxes: Vec<_> = boxes_for(&obstacles, |o| o["kind"].as_str().map(|k| k.contains("via") || k.contains("hole")).unwrap_or(false)).collect();+ let uncovered_vias = board.vias().iter().filter(|v| {+ let r = v.size / 2.0;+ !via_boxes.iter().any(|&b| inside((v.x - r, v.y - r), b) && inside((v.x + r, v.y + r), b))+ }).count();+ let mut why = Vec::new();+ if !short.is_empty() {+ why.push(format!("the obstacle boxes of {} footprint(s) do not cover their own pads on the board ({}{}): obstacles must be the parts' real extents measured from the board: every pad's copper, mask and holes, and each part's fitted body", short.len(), short.iter().take(12).cloned().collect::<Vec<_>>().join(", "), if short.len() > 12 { ", ..." } else { "" }));+ }+ if uncovered_vias > 0 {+ why.push(format!("{uncovered_vias} of the board's vias have no via/hole obstacle box covering them"));+ }+ why+}+ #[cfg(test)] mod tests { use super::*;@@ -55,7 +100,8 @@ mod tests { #[test] fn records_only_a_passing_audit_that_covers_the_board() { let b = Board::from_text("t.kicad_pcb", BOARD.to_string()).unwrap();- let input = json!({"inventory": [{"id": "R1", "kind": "body", "layers": ["F.SilkS"]}, {"id": "C1:body", "kind": "body", "layers": ["F.SilkS"]}, {"id": "v1", "kind": "via", "layers": ["F.SilkS", "B.SilkS"]}]});+ let input = json!({"inventory": [{"id": "R1", "kind": "body", "layers": ["F.SilkS"]}, {"id": "C1:body", "kind": "body", "layers": ["F.SilkS"]}, {"id": "v1", "kind": "via", "layers": ["F.SilkS", "B.SilkS"]}],+ "obstacles": [{"id": "R1", "kind": "body", "box": [1.4, 1.4, 2.6, 2.6]}, {"id": "C1:body", "kind": "body", "box": [5.4, 1.4, 6.6, 2.6]}, {"id": "v1", "kind": "via", "box": [4.7, 4.7, 5.3, 5.3]}]}); let passed = json!({"passed": true, "inventoryItems": 3, "obstacles": 4, "marks": 4}); let p = proof(&passed, &input, &b).unwrap(); assert_eq!(p["footprints"], 2);@@ -67,6 +113,28 @@ mod tests { assert!(why.contains("does not name 1") && why.contains(": R1") && why.contains("0 via/hole"), "{why}"); } + /// Opt-in check on a real audit: AIFLOW_SILK_INPUT=<audit-input.json> AIFLOW_SILK_BOARD=<board.kicad_pcb>+ #[test]+ #[ignore]+ fn geometry_on_a_real_audit() {+ let input = serde_json::from_str::<Value>(&std::fs::read_to_string(std::env::var("AIFLOW_SILK_INPUT").unwrap()).unwrap()).unwrap();+ let b = Board::load(&std::env::var("AIFLOW_SILK_BOARD").unwrap()).unwrap();+ let gaps = geometry_gaps(&input, &b);+ eprintln!("gaps: {gaps:?}");+ assert_eq!(gaps.is_empty(), std::env::var("AIFLOW_SILK_WANT").map(|w| w == "pass").unwrap_or(true));+ }++ #[test]+ fn point_sized_obstacles_are_refused() {+ // what the 2026-10-15 run supplied: 0.4 mm boxes at each part, nothing for the via's real size+ let b = Board::from_text("t.kicad_pcb", BOARD.to_string()).unwrap();+ let input = json!({"inventory": [{"id": "R1", "kind": "body"}, {"id": "C1", "kind": "body"}, {"id": "v1", "kind": "via"}],+ "obstacles": [{"id": "R1", "kind": "body", "box": [1.8, 1.8, 2.2, 2.2]}, {"id": "C1", "kind": "body", "box": [5.4, 1.4, 6.6, 2.6]}, {"id": "v1", "kind": "via", "box": [4.9, 4.9, 5.1, 5.1]}]});+ let why = proof(&json!({"passed": true}), &input, &b).unwrap_err().join("; ");+ assert!(why.contains("1 footprint(s) do not cover their own pads on the board (R1)"), "{why}");+ assert!(why.contains("1 of the board's vias"), "{why}");+ }+ #[test] fn a_bare_board_is_not_a_silkscreen() { let bare = BOARD.replace("(layer \"F.SilkS\")", "(layer \"F.Fab\")");
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.96",+ "version": "0.1.97", "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 today; Altium, Fusion and 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.1.96",+ "version": "0.1.97", "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 today; Altium, Fusion and 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.",