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
Merge PR #456: AI Flow 0.2.60: export routes the order to the fab's stage (closes #184) (aiflow-0.2.60 -> main) merge
Showing changes introduced by the merge (against its first parent).
9 files changed
+101−17
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.2.59"+version = "0.2.60" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.2.59"+version = "0.2.60" dependencies = [ "aiflow-board", "serde",@@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.2.59"+version = "0.2.60" dependencies = [ "roxmltree", "serde",@@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.2.59"+version = "0.2.60" dependencies = [ "aiflow-board", "serde",@@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.2.59"+version = "0.2.60" dependencies = [ "aiflow-board", "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.2.59"+version = "0.2.60" dependencies = [ "aiflow-board", "serde",@@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.2.59"+version = "0.2.60" dependencies = [ "aiflow-board", "serde",@@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.2.59"+version = "0.2.60" dependencies = [ "aiflow-board", "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.2.59"+version = "0.2.60" dependencies = [ "aiflow-board", "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.2.59"+version = "0.2.60" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.2.59"+version = "0.2.60" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/export.rs+56@@ -393,6 +393,45 @@ pub fn stale_fills(drc: &Value) -> Value { /// The hint for stale saved fills: refill, save, export again. pub const STALE_FILLS_HINT: &str = "Refill the zones and save the board: in KiCad Edit > Fill All Zones (or `kicad_refill_zones` through the bridge) then save; in a pcbnew script `pcbnew.ZONE_FILLER(board).Fill(board.Zones())` before `pcbnew.SaveBoard(...)`. Then run `export` again: the Gerbers and STEP plot the saved fills as they are, and DRC with --refill-zones (the gate's) cannot see this."; +/// The note beside the set that says which file is for whom (issue #184).+pub const ORDER_NOTE: &str = "ORDER-FILES.md";++fn on_path(bin: &str) -> bool {+ std::env::var_os("PATH").map(|p| std::env::split_paths(&p).any(|d| d.join(bin).is_file())).unwrap_or(false)+}++/// Issue #184: what each file in the set is for, and where a fab's own BOM and CPL come from.+/// The Gerbers and drill files are the formats every fab's upload takes. The BOM and CPL are AI+/// Flow's review files: checked against the board, KiCad's rotations and origins, and not any+/// fab's upload format. JLCPCB places each part from its own (EasyEDA) footprint, whose zero+/// rotation and origin differ from KiCad's on many packages, so its CPL comes from the JLCPCB+/// ordering stage, which fits every part to JLCPCB's footprint. `profile` is the run's fab+/// profile, `sourcing_bom` the BOM the run's sourcing check passed on.+/// Returns the record for the report, the note's text, and the hints (the profile's fab first).+pub fn order_files(board_path: &str, stem: &str, profile: Option<&str>, sourcing_bom: Option<&str>) -> (Value, String, Vec<String>) {+ let bom_arg = sourcing_bom.unwrap_or("<the sourcing BOM: ref, mpn and lcsc columns>");+ let jlc_cmd = format!("adom-aiflow-jlcpcb init --board {board_path} --bom {bom_arg} --target <desktop>, then `adom-aiflow-jlcpcb next`");+ let jlc_install = if on_path("adom-aiflow-jlcpcb") { "" } else { " (install it first: `adom-wiki pkg install adom/adom-aiflow-jlcpcb`)" };+ let pcbway_install = if on_path("adom-aiflow-pcbway") { "" } else { " (install: `adom-wiki pkg install adom/adom-aiflow-pcbway`)" };+ let jlc_line = format!("Ordering at JLCPCB: `{jlc_cmd}`{jlc_install}. Its export writes {stem}-bom-jlcpcb.csv (Comment, Designator, Footprint, LCSC Part #) and {stem}-cpl-jlcpcb.csv (Designator, Mid X, Mid Y, Layer, Rotation), fits every part's rotation and centroid to JLCPCB's own footprint, and refuses a placed part with no LCSC number.");+ let pcbway_line = format!("Ordering at PCBWay: `adom-aiflow-pcbway init` then `adom-aiflow-pcbway next`{pcbway_install}: exact-MPN BOM and centroid for PCBWay's quote.");+ let other_line = "Any other fab: send the gerbers folder; give it the BOM and CPL in its own columns, and check every part's pin 1 in its placement preview before paying.".to_string();+ let mut hints = vec![format!("{stem}-bom.csv and {stem}-cpl.csv are AI Flow's review files (KiCad rotations and origins), not a fab's upload format: no fab's assembly upload takes them as they are. {} says which file goes where.", ORDER_NOTE)];+ hints.extend([jlc_line.clone(), pcbway_line.clone(), other_line.clone()]);+ let fab = match profile { Some("jlcpcb") => "jlcpcb (JLCPCB parts preferred)", Some(_) => "fab (the 3rd party fab profile)", None => "not named in the spec or requirements" };+ let note = format!("# Order files for {stem}\n\nWritten by `adom-aiflow export`. Fab profile of this run: {fab}.\n\n\+| File | For |\n|---|---|\n\+| `gerbers/` (copper, mask, paste, silkscreen, Edge.Cuts; Excellon drill) | Every fab: zip the folder for the upload. |\n\+| `{stem}-bom.csv` | Review only: AI Flow's columns (Reference, Qty, Value, Footprint, Manufacturer, MPN, DNP), checked against the board. Not a fab's upload format. |\n\+| `{stem}-cpl.csv` | Review only: kicad-cli's placement file (Ref, Val, Package, PosX, PosY, Rot, Side), KiCad's rotations and origins. Not a fab's upload format: a fab places from its own footprints, whose zero rotation and origin differ on many packages. |\n\+| `export-check.json` | The checks behind this set. |\n\n\+## The assembly files for the fab you order from\n\n- {jlc_line}\n- {pcbway_line}\n- {other_line}\n");+ let rec = json!({"reviewOnly": [format!("{stem}-bom.csv"), format!("{stem}-cpl.csv")], "fabUploadReady": ["gerbers/"],+ "fabProfile": profile, "note": ORDER_NOTE,+ "next": {"jlcpcb": jlc_cmd, "pcbway": "adom-aiflow-pcbway init"}});+ (rec, note, hints)+}+ fn saved_drc(cli: &str, board_path: &str, out: &Path) -> Result<Value, String> { let report = out.join("drc-saved-fills.json"); let _ = std::fs::remove_file(&report);@@ -614,6 +653,23 @@ mod tests { assert_eq!(r["pass"], true); assert_eq!(r["otherErrors"], 1); } + #[test]+ fn order_files_say_the_bom_and_cpl_are_for_review_and_route_to_the_fab_stage() {+ // issue #184: the review files are never presented as a fab's upload files+ let (rec, note, hints) = order_files("/p/b.kicad_pcb", "b", Some("fab"), Some("/p/design/bom.csv"));+ assert_eq!(rec["reviewOnly"], json!(["b-bom.csv", "b-cpl.csv"]));+ assert!(hints[0].contains("not a fab's upload format"));+ assert!(note.contains("| `b-cpl.csv` | Review only") && note.contains("Not a fab's upload format"));+ let jlc = rec["next"]["jlcpcb"].as_str().unwrap();+ assert!(jlc.starts_with("adom-aiflow-jlcpcb init --board /p/b.kicad_pcb --bom /p/design/bom.csv"), "{jlc}");+ assert!(note.contains("b-cpl-jlcpcb.csv (Designator, Mid X, Mid Y, Layer, Rotation)") && note.contains("JLCPCB's own footprint"));+ assert!(note.contains("3rd party fab"));+ // no sourcing BOM recorded: the command says what the BOM needs+ let (rec, note, _) = order_files("b.kicad_pcb", "b", Some("jlcpcb"), None);+ assert!(rec["next"]["jlcpcb"].as_str().unwrap().contains("lcsc columns"));+ assert!(note.contains("jlcpcb (JLCPCB parts preferred)"));+ }+ #[test] fn csv_reader_handles_quotes() { let rows = read_csv("a,\"b,c\",\"d \"\"e\"\"\"\r\n\n1,2,3");
crates/adom-aiflow/src/main.rs+12−4@@ -3509,13 +3509,21 @@ fn main() { let kicad = std::env::var("ADOM_AIFLOW_KICAD_CLI").unwrap_or_else(|_| "kicad-cli".into()); let off = export::off_board(&spec_soft(&r)); match export::export_with(&board_path, &out_dir, &kicad, &off) {- Ok(report) => {+ Ok(mut report) => {+ // issue #184: say which file is for whom; a fab's BOM and CPL come from its ordering stage+ let stem = Path::new(&board_path).file_stem().and_then(|s| s.to_str()).unwrap_or("board").to_string();+ let profile = preboard::named_profile(&spec_soft(&r)).or_else(|| preboard::named_profile(&requirements_of(&r)));+ let (order, note, order_hints) = export::order_files(&board_path, &stem, profile.as_deref(), run_bom(&r).as_deref());+ if let Err(e) = std::fs::write(out_dir.join(export::ORDER_NOTE), ¬e) { err(&format!("{}: {e}", out_dir.join(export::ORDER_NOTE).display()), &[]); }+ report["orderFiles"] = order; write_json(&out_dir.join("export-check.json"), &report); r.outcome("export", report.clone()); r.save().unwrap();- let mut hints: Vec<String> = report["files"].as_array().into_iter().flatten().filter_map(|f| f.as_str()).map(|f| format!("file: {f}")).collect();+ let mut hints = order_hints;+ hints.extend(report["files"].as_array().into_iter().flatten().filter_map(|f| f.as_str()).map(|f| format!("file: {f}")));+ hints.push(format!("file: {}", out_dir.join(export::ORDER_NOTE).display())); hints.extend(report["notes"].as_array().into_iter().flatten().filter_map(|n| n.as_str()).map(str::to_string));- hints.push("Tell the human where every file is. Bottom-side CPL rotations follow kicad-cli's convention and were counted, not compared; check them against the fab's preview.".into());+ hints.push("Tell the human where every file is and which ones a fab's upload takes (the gerbers folder only), and that the BOM and CPL here are for review.".into()); if let Some(h) = molecule_publish_hint(&r) { hints.push(h); } // issue #160: the saved fills are older than the copper: a finding, the files are written if report["fillsCurrent"]["pass"] == false {@@ -3526,7 +3534,7 @@ fn main() { } let facts = Board::load(&board_path).ok().filter(|_| mechanical::applies(&board_path)).map(|b| verified::board_facts(&b, &board_path, &project_dir_of(&r), &spec_soft(&r))); let checked = verified::run_items(&r, facts.as_ref());- ok(&format!("manufacturing files in {}, checked against the board: Gerbers ({} copper layers of {}, pours present as regions on {} poured layer(s)), drill ({} hits = vias and drilled pads), pick and place ({} parts), BOM ({} lines)\n{}", out_dir.display(), report["gerbers"]["copperLayers"], report["gerbers"]["boardCopperLayers"], report["fillsGerbers"]["layersWithPours"].as_object().map(|o| o.len()).unwrap_or(0), report["drill"]["hits"], report["cpl"]["refs"], report["bom"]["lines"], verified::message(&checked)), &hints);+ ok(&format!("manufacturing files in {}, checked against the board: Gerbers ({} copper layers of {}, pours present as regions on {} poured layer(s)), drill ({} hits = vias and drilled pads), pick and place ({} parts) and BOM ({} lines) for review, not a fab's upload format (a fab's own come from its ordering stage; {} says which file goes where)\n{}", out_dir.display(), report["gerbers"]["copperLayers"], report["gerbers"]["boardCopperLayers"], report["fillsGerbers"]["layersWithPours"].as_object().map(|o| o.len()).unwrap_or(0), report["drill"]["hits"], report["cpl"]["refs"], report["bom"]["lines"], export::ORDER_NOTE, verified::message(&checked)), &hints); } Err((report, problems)) => { let _ = std::fs::create_dir_all(&out_dir);
docs/release-0.2.60.mdadded+13@@ -0,0 +1,13 @@+# AI Flow 0.2.60++- **`export` no longer passes its BOM and CPL off as order files** (issue #184). `<board>-bom.csv` and+ `<board>-cpl.csv` are AI Flow's review files: AI Flow's columns, kicad-cli's placement file with KiCad's+ rotations and origins, checked against the board. No fab's assembly upload takes them, and a fab places each+ part from its own footprint, whose zero rotation and origin differ from KiCad's on many packages. `export`+ now says so in its answer, writes `manufacturing/ORDER-FILES.md` (which file is for whom), records+ `orderFiles` in `export-check.json`, and its first hints give the ordering stage's command with the run's+ board and sourcing BOM filled in: `adom-aiflow-jlcpcb init --board ... --bom ... --target <desktop>` for+ JLCPCB (its export writes JLCPCB's BOM and CPL with every rotation and centroid fitted to JLCPCB's own+ footprint, and refuses a part with no LCSC number), `adom-aiflow-pcbway` for PCBWay.+- Works with adom-aiflow-jlcpcb 0.1.4, which reads AI Flow's BOM columns as they are and never takes a Mouser+ number for an LCSC number.
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.2.59",+ "version": "0.2.60", "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.59",+ "version": "0.2.60", "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+7@@ -130,6 +130,13 @@ findings that name a zone mean the saved fills are older than the copper (an off saved without `pcbnew.ZONE_FILLER(board).Fill(board.Zones())`); the Gerbers plot those fills, so export answers FINDINGS with the refill hint and finish shows "fills in Gerbers NO" until you refill, save and export again. Any pcbnew script that edits copper must refill before `SaveBoard`. Bottom-side CPL rotations are counted, not compared; check them in the fab's preview.+**The BOM and CPL that `export` writes are for review, not for a fab's upload** (issue #184): AI Flow's+columns and KiCad's rotations and origins. No fab's assembly upload takes them as they are, and a fab places+each part from its own footprint, whose zero rotation and origin differ from KiCad's on many packages.+`export` writes `manufacturing/ORDER-FILES.md` saying which file goes where, and names the ordering stage:+for JLCPCB, `adom-aiflow-jlcpcb init --board <board> --bom <sourcing BOM> --target <desktop>` (its export+writes the JLCPCB BOM and CPL with every rotation fitted to JLCPCB's footprint); for PCBWay,+`adom-aiflow-pcbway`. Never hand the human `<board>-bom.csv` or `<board>-cpl.csv` as a fab's order files. ## The timeline (show it, keep it up)