← All Pull Requests

AI Flow 0.2.61: export reports a mismatched set as findings #78

Merged Released in 0.2.61 on 10 Nov 2026 opened by Colby Knox
Merges aiflow-0.2.61 → main

AI Flow 0.2.61: export reports a set that doesn't match the board as findings instead of stopping (the dynamic-skill rule from #77).

  • When the Gerbers, drill, CPL or BOM that export wrote don't check out against the board, it answers FINDINGS with the problems and the file paths and records the export. The verified list then reads "NO (the manufacturing files do not match the board: ...)", so finish and deliver still say the board isn't cleared for fabrication.
  • ERROR now means only that kicad-cli couldn't write the set.
  • Test: a_written_set_that_does_not_match_the_board_reads_no. 671 workspace tests, the tool tests and the preflight pass.
  • Pairs with adom-aiflow-jlcpcb 0.1.5 and adom-aiflow-pcbway 0.1.3, which follow the same rule.

🤖 Generated with Claude Code

Diff Skip to comments

Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.2.60"+version = "0.2.61" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.2.60"+version = "0.2.61" dependencies = [  "aiflow-board",  "serde",@@ -35,7 +35,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.2.60"+version = "0.2.61" dependencies = [  "roxmltree",  "serde",@@ -44,7 +44,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.2.60"+version = "0.2.61" dependencies = [  "aiflow-board",  "serde",@@ -53,7 +53,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.2.60"+version = "0.2.61" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.2.60"+version = "0.2.61" dependencies = [  "aiflow-board",  "serde",@@ -72,7 +72,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.2.60"+version = "0.2.61" dependencies = [  "aiflow-board",  "serde",@@ -81,7 +81,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.2.60"+version = "0.2.61" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.2.60"+version = "0.2.61" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.2.60"+version = "0.2.61" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.2.60"+version = "0.2.61" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+12−2
@@ -3539,10 +3539,20 @@ fn main() {                 Err((report, problems)) => {                     let _ = std::fs::create_dir_all(&out_dir);                     write_json(&out_dir.join("export-check.json"), &report);+                    let fill_hint = if problems.iter().any(|p| p.contains("filled polygon") || p.contains("G36")) { fills::HINT.to_string() } else { String::new() };+                    // a dynamic skill: a set that was written but does not match the board is a finding, recorded+                    // (the verified list says NO), never a stop; only kicad-cli not running is an error+                    if report.get("files").is_some() {+                        r.outcome("export", report.clone());+                        r.log("export-findings", json!({"board": board_path, "dir": out_dir, "problems": problems}));+                        r.save().unwrap();+                        let mut hints = vec![fill_hint, "Fix the board or its footprint attributes (DNP, exclude from BOM/position files) and run `export` again. Until then, tell the human which files do not match the board and why before anything is ordered.".into()];+                        hints.extend(report["files"].as_array().into_iter().flatten().filter_map(|f| f.as_str()).map(|f| format!("file: {f}")));+                        findings(&format!("manufacturing files written to {}, but they do not check out against the board: {}", out_dir.display(), problems.join("; ")), &hints);+                    }                     r.log("export-refused", json!({"board": board_path, "dir": out_dir, "problems": problems}));                     r.save().unwrap();-                    let fill_hint = if problems.iter().any(|p| p.contains("filled polygon") || p.contains("G36")) { fills::HINT.to_string() } else { String::new() };-                    err(&format!("manufacturing files do not check out against the board: {}", problems.join("; ")), &[fill_hint, "Fix the board or its footprint attributes (DNP, exclude from BOM/position files) and run `export` again; never hand the human files that do not match the board.".into(), "kicad-cli: ADOM_AIFLOW_KICAD_CLI picks it; in a container with no native KiCad, ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote KICAD_REMOTE_TARGET=<desktop>.".into()]);+                    err(&format!("manufacturing files could not be written: {}", problems.join("; ")), &[fill_hint, "Check that kicad-cli runs and can read the board, then run `export` again.".into(), "kicad-cli: ADOM_AIFLOW_KICAD_CLI picks it; in a container with no native KiCad, ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote KICAD_REMOTE_TARGET=<desktop>.".into()]);                 }             }         }
crates/adom-aiflow/src/verified.rs+11−1
@@ -123,7 +123,9 @@ pub fn from_run(outcomes: &Map<String, Value>, data: &Value, mech: Option<(bool,     let erc = match erc_rec { Some(p) => yes_no(p, "ERC failed", "ERC result states no pass"), None if no_sch => State::NotApplicable("pattern board, no schematic".into()), None => State::Unknown("no ERC result recorded in the run; `adom-aiflow netlist check` runs KiCad's ERC on the run's schematic and records it".into()) };     // the pattern lane skips the gate: its DRC is finish's recorded live full DRC, never assumed     let drc = if pattern { live_drc } else { gate.map(|g| yes_no(g["newErrors"].as_u64().map(|n| n == 0), "DRC errors remain", "not recorded")).unwrap_or(State::Unknown("gate not run".into())) };-    let gerbers = outcomes.get("export").map(|e| if e["fillsCurrent"]["pass"] == false { State::No("the saved zone fills are stale: DRC without refill finds zone violations; refill, save and export again".into()) } else { yes_no(e["fillsGerbers"]["pass"].as_bool(), "a poured layer has no Gerber regions", "export predates this check; run export again") }).unwrap_or(State::Unknown("not exported yet (run export)".into()));+    let gerbers = outcomes.get("export").map(|e| if e["pass"] == false && e["files"].is_array() {+        State::No(format!("the manufacturing files do not match the board: {}", e["problems"].as_array().map(|a| a.iter().filter_map(|p| p.as_str()).collect::<Vec<_>>().join("; ")).unwrap_or_default()))+    } else if e["fillsCurrent"]["pass"] == false { State::No("the saved zone fills are stale: DRC without refill finds zone violations; refill, save and export again".into()) } else { yes_no(e["fillsGerbers"]["pass"].as_bool(), "a poured layer has no Gerber regions", "export predates this check; run export again") }).unwrap_or(State::Unknown("not exported yet (run export)".into()));     vec![         Item { name: "schematic parity", state: parity },         Item { name: "connectivity", state: connectivity },@@ -276,6 +278,14 @@ mod tests {         assert!(v.starts_with("Not cleared for fabrication") && v.contains("ERC") && v.contains("body overlap"), "{v}");     } +    #[test]+    fn a_written_set_that_does_not_match_the_board_reads_no() {+        let o: Map<String, Value> = serde_json::from_value(json!({"export": {"pass": false, "files": ["b-cpl.csv"], "problems": ["pick and place lacks R2"], "fillsGerbers": {"pass": true}}})).unwrap();+        let items = from_run(&o, &json!({}), None, None);+        let g = items.iter().find(|i| i.name == "fills in Gerbers").unwrap();+        assert!(matches!(&g.state, State::No(w) if w.contains("lacks R2")), "{:?}", g.state);+    }+     #[test]     fn stale_saved_fills_say_no_on_the_gerbers() {         // issue #160: fills present in the Gerbers but older than the copper
docs/release-0.2.61.mdadded+9
@@ -0,0 +1,9 @@+# AI Flow 0.2.61++- **`export` reports a mismatched set instead of stopping** (the dynamic-skill rule, PR #77). When the Gerbers, drill,+  CPL or BOM it wrote do not check out against the board, it answers FINDINGS with the problems and the files, records+  the export, and the verified list reads "NO (the manufacturing files do not match the board: ...)", so `finish` and+  `deliver` still say the board is not cleared for fabrication. An ERROR now means only that kicad-cli could not write+  the set.+- The ordering stages follow the same rule: adom-aiflow-jlcpcb 0.1.5 and adom-aiflow-pcbway 0.1.3 report findings+  instead of refusing, and PCBWay's reads AI Flow's own export BOM and CPL as they are.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.60",+  "version": "0.2.61",   "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.60",+  "version": "0.2.61",   "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.",

Comments

No comments yet.

Log in to comment.