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 #464: AI Flow 0.2.63: silkscreen proof takes a live deviation for the label count (aiflow-0.2.63 -> main) merge
Showing changes introduced by the merge (against its first parent).
9 files changed
+74−19
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.2.62"+version = "0.2.63" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.2.62"+version = "0.2.63" dependencies = [ "aiflow-board", "serde",@@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.2.62"+version = "0.2.63" dependencies = [ "roxmltree", "serde",@@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.2.62"+version = "0.2.63" dependencies = [ "aiflow-board", "serde",@@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.2.62"+version = "0.2.63" dependencies = [ "aiflow-board", "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.2.62"+version = "0.2.63" dependencies = [ "aiflow-board", "serde",@@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.2.62"+version = "0.2.63" dependencies = [ "aiflow-board", "serde",@@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.2.62"+version = "0.2.63" dependencies = [ "aiflow-board", "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.2.62"+version = "0.2.63" dependencies = [ "aiflow-board", "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.2.62"+version = "0.2.63" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.2.62"+version = "0.2.63" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+9−2@@ -3434,9 +3434,15 @@ fn main() { for k in ["powerInputs", "testPoints"] { audit[k] = labels[k].clone(); } std::fs::write(out, serde_json::to_string_pretty(&audit).unwrap()).unwrap_or_else(|e| err(&format!("{}: {e}", out.display()), &[])); let data = read_json(&input.display().to_string());- let mut proof = silkproof::proof(&audit, &data, &b).unwrap_or_else(|why| {+ // issue #194: a live deviation on the run's current board and spec answers the label count only.+ // finish names the count "silkscreen" and a dropped proof "silkscreen proof"; either one counts.+ let waiver = if board_path == r.current_board() {+ let (bs, ss) = run_hashes(&r);+ deviations::live(&r.data, &bs, &ss).into_iter().find(|d| matches!(d["item"].as_str(), Some("silkscreen proof" | "silkscreen")))+ } else { None };+ let mut proof = silkproof::proof_with(&audit, &data, &b, waiver.as_ref()).unwrap_or_else(|why| { r.log("silkscreen-proof-refused", json!({"board": board_path, "audit": out, "why": why}));- err(&format!("silkscreen proof not recorded: {}", why.join("; ")), &["Build the inventory from the whole board (every footprint by its reference, every via and hole on both faces) and fix the labels, then `silkscreen-audit --record` again.".into()])+ err(&format!("silkscreen proof not recorded: {}", why.join("; ")), &["Build the inventory from the whole board (every footprint by its reference, every via and hole on both faces) and fix the labels, then `silkscreen-audit --record` again.".into(), "A face meant to carry no references (a nameplate, a cosmetic side): `deviate \"silkscreen proof\" --why \"...\"` answers the label count only; the audit, coverage and geometry still have to pass.".into()]) }); proof["board"] = json!(board_path); proof["boardSha256"] = json!(sha256_file(&board_path));@@ -3453,6 +3459,7 @@ fn main() { // 0.2.18: what the labels say, not only where they sit: power inputs without a voltage, test points without their net for w in silk_polarity::warnings(&b).iter().filter(|w| w.contains("power input") || w.contains("test point")) { eprintln!("Warning: {w}"); } for f in fab["findings"].as_array().into_iter().flatten().filter_map(Value::as_str) { eprintln!("Warning (Fab layer, not blocking): {f}. {}", fab_layer::TIDY_HINT); }+ if !proof["labelDensityWaived"].is_null() { eprintln!("Note: the label count ({} for {} footprints) is answered by the deviation \"silkscreen proof\": {}", proof["labelDensityWaived"]["labels"], proof["labelDensityWaived"]["footprints"], proof["labelDensityWaived"]["why"]); } ok(&format!("{}silkscreen step proof recorded: {} footprints and {} vias covered, {} labels\n{}", String::from_utf8_lossy(&result.stdout), proof["footprints"], proof["vias"], proof["labels"], silk_polarity::label_check_lines(&labels).join("\n")), &["Geometry audit only; native DRC, label association and both native 2D/3D views remain required. Then `step 3d`.".into()]); return; }
crates/adom-aiflow/src/silkproof.rs+40−3@@ -14,6 +14,14 @@ fn covers(id: &str, reference: &str) -> bool { /// The proof's evidence, or why it cannot be recorded. pub fn proof(audit: &Value, input: &Value, board: &Board) -> Result<Value, Vec<String>> {+ proof_with(audit, input, board, None)+}++/// `proof`, with the label-density criterion answered by a live `silkscreen proof` deviation (bound to this+/// board and spec): a face that is meant to carry no references (a desk object's display face or back, a+/// product's cosmetic side) still records the geometry proof. Only the count is waived: the audit must pass,+/// cover every footprint and via, and match the board's geometry; the waiver and its reason go into the proof.+pub fn proof_with(audit: &Value, input: &Value, board: &Board, density_waiver: Option<&Value>) -> Result<Value, Vec<String>> { let mut why = Vec::new(); if audit["passed"] != true { why.push("the audit did not pass".to_string()); } let inventory = input["inventory"].as_array().cloned().unwrap_or_default();@@ -28,16 +36,23 @@ pub fn proof(audit: &Value, input: &Value, board: &Board) -> Result<Value, Vec<S 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);+ let mut waived = Value::Null; 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));+ match density_waiver {+ Some(d) => waived = json!({"labels": labels, "footprints": fps, "minimumPerFootprint": crate::provenance::MIN_SILK_PER_FOOTPRINT,+ "deviation": d["item"], "why": d["why"], "at": d["at"], "aiThread": d["aiThread"]}),+ None => why.push(format!("{labels} visible silkscreen label(s) for {fps} footprints (at least {:.1} per footprint)", crate::provenance::MIN_SILK_PER_FOOTPRINT)),+ } } if !why.is_empty() { return Err(why); }- Ok(json!({+ let mut p = json!({ "pass": true, "footprints": refs.len(), "footprintsCovered": refs.len(), "vias": vias, "viaItems": via_items, "labels": labels, "inventoryItems": audit["inventoryItems"], "obstacles": audit["obstacles"], "marks": audit["marks"], "collisions": 0, "nativeVerified": false, "note": "geometry audit of the supplied whole-board inventory; native DRC, label association and both native 2D/3D views remain required",- }))+ });+ if !waived.is_null() { p["labelDensityWaived"] = waived; }+ Ok(p) } /// How far a supplied box may fall short of the board's own geometry (mm).@@ -223,6 +238,28 @@ mod tests { (via (at 5 5) (size 0.6) (drill 0.3) (layers "F.Cu" "B.Cu")) )"#; + #[test]+ fn a_live_deviation_waives_only_the_label_count() {+ 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"]}],+ "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});+ // the same board with every silkscreen text hidden: no labels at all+ let bare = Board::from_text("t.kicad_pcb", BOARD.replace("(layer \"F.SilkS\")", "(layer \"F.SilkS\") (hide yes)")).unwrap();+ assert_eq!(crate::provenance::silk_labels(&bare).0, 0);+ {+ assert!(proof(&passed, &input, &bare).unwrap_err().join("; ").contains("per footprint"));+ let dev = json!({"item": "silkscreen proof", "why": "a desk object's display face: no references on purpose", "at": "2026-11-10T00:00:00Z", "aiThread": "t"});+ let p = proof_with(&passed, &input, &bare, Some(&dev)).unwrap();+ assert_eq!(p["labelDensityWaived"]["labels"], 0);+ assert_eq!(p["labelDensityWaived"]["deviation"], "silkscreen proof");+ // the waiver never covers anything else: a failed audit stays refused+ assert!(proof_with(&json!({"passed": false}), &input, &bare, Some(&dev)).unwrap_err()[0].contains("did not pass"));+ }+ // a board that meets the count records no waiver+ assert!(proof_with(&passed, &input, &b, None).unwrap().get("labelDensityWaived").is_none());+ }+ #[test] fn records_only_a_passing_audit_that_covers_the_board() { let b = Board::from_text("t.kicad_pcb", BOARD.to_string()).unwrap();
docs/release-0.2.63.mdadded+8@@ -0,0 +1,8 @@+# AI Flow 0.2.63++- **#194: a board meant to carry no references can record its silkscreen proof.** `silkscreen-audit --record`+ takes a live `deviate "silkscreen proof"` (or `deviate "silkscreen"`, the item `finish` names) on the run's+ current board and spec as the answer to the label count, and only the label count: the audit itself,+ coverage of every footprint and via, and the geometry check against the board still have to pass. The+ proof records `labelDensityWaived` with the counts, the deviation and its reason, and the command prints+ a Note naming it. Patch and test by Noah. The refusal's hint now says how.
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.2.62",+ "version": "0.2.63", "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.62",+ "version": "0.2.63", "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+4−1@@ -132,7 +132,10 @@ cleared. On the KiCad lane, `silkscreen-audit --input <audit.json> --out <result.json> --record` records the silkscreen proof only when the audit passes and its inventory covers the board: every footprint by its reference (id `R1`, or `R1:body`, `R1-value`), a via or hole item for every via, and at least 0.5 visible-labels per footprint. `export` writes the manufacturing set with kicad-cli (ADOM_AIFLOW_KICAD_CLI;+labels per footprint. A board whose faces are meant to carry no references (a nameplate, a cosmetic+back with the references on F.Fab) answers only that count with a live `deviate "silkscreen proof" --why+"..."`; the audit, coverage and geometry still have to pass, and the proof records `labelDensityWaived`.+`export` writes the manufacturing set with kicad-cli (ADOM_AIFLOW_KICAD_CLI; `adom-aiflow-kicad-cli-remote` in a container): Gerbers and drill in `<run>/manufacturing/gerbers/`, the CPL and a BOM built from the board's footprints beside them. It reads every file back (copper layers from the Gerber file functions, drill hits against vias and drilled pads, CPL and BOM references, sides and top