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.
← All Pull Requests
AI Flow 0.2.43: a polarized part's + and - pins are not amplifier inputs (#126) #58
Merges
aiflow-lane-0.2.43 → mainCloses #126.
A part with one or two terminals has no amplifier inputs. Bare + and - count as amplifier inputs only on an op amp or comparator by description, or beside an output pin. amp_like itself returned true for any symbol with + and - pins, so the issue's suggested gate alone would not have fixed it. IN+ and IN- on three or more pins still count without an output.
Base: bef376b3. Tested: 538 Rust tests and the Python suites pass; no CLI command is missing against 0.2.42. With the real adom/eeefth101xap symbol through a real kicad-cli, 0.2.42 gives both errors and this build gives neither.
🤖 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.42"+version = "0.2.43" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.2.42"+version = "0.2.43" dependencies = [ "aiflow-board", "serde",@@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.2.42"+version = "0.2.43" dependencies = [ "roxmltree", "serde",@@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.2.42"+version = "0.2.43" dependencies = [ "aiflow-board", "serde",@@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.2.42"+version = "0.2.43" dependencies = [ "aiflow-board", "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.2.42"+version = "0.2.43" dependencies = [ "aiflow-board", "serde",@@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.2.42"+version = "0.2.43" dependencies = [ "aiflow-board", "serde",@@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.2.42"+version = "0.2.43" dependencies = [ "aiflow-board", "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.2.42"+version = "0.2.43" dependencies = [ "aiflow-board", "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.2.42"+version = "0.2.43" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.2.42"+version = "0.2.43" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
crates/adom-aiflow/src/schematic_symbols.rs+54−4@@ -181,10 +181,28 @@ fn is_power(name: &str) -> bool { || (n.ends_with("GND") && n.len() <= 5) } +/// A part with one or two terminals has no amplifier inputs: `+` and `-` on a polarized capacitor, a+/// battery or a polarized connector name its terminals (issue #126).+fn two_terminal(s: &LibSym) -> bool {+ s.pins.iter().map(|p| p.number.as_str()).collect::<BTreeSet<_>>().len() <= 2+}++/// A pin named bare `+` or `-`, which says nothing about amplifiers on its own, unlike `IN+`.+fn bare_sign(name: &str) -> bool { matches!(norm(name).as_str(), "+" | "-") }+ fn amp_like(s: &LibSym) -> bool {+ if two_terminal(s) { return false; } let i = &s.info;- ["opamp", "op amp", "op-amp", "operational", "comparator"].iter().any(|k| i.contains(k))- || (s.pins.iter().any(|p| amp_input(&p.name) == Some('+')) && s.pins.iter().any(|p| amp_input(&p.name) == Some('-')))+ if ["opamp", "op amp", "op-amp", "operational", "comparator"].iter().any(|k| i.contains(k)) { return true; }+ let both = s.pins.iter().any(|p| amp_input(&p.name) == Some('+')) && s.pins.iter().any(|p| amp_input(&p.name) == Some('-'));+ // bare +/- make an amplifier only with an output beside them: a `+ - NTC` battery connector is not one+ both && s.pins.iter().any(|p| (amp_input(&p.name).is_some() && !bare_sign(&p.name)) || is_output(&p.name) || ["output", "open_collector", "open_emitter", "tri_state"].contains(&p.etype.as_str()))+}++/// The sign of a pin that really is an amplifier input on this symbol.+fn amp_input_on(s: &LibSym, name: &str) -> Option<char> {+ if two_terminal(s) || (bare_sign(name) && !amp_like(s)) { return None; }+ amp_input(name) } fn ref_prefix(r: &str) -> String { r.chars().take_while(|c| c.is_ascii_alphabetic()).collect::<String>().to_ascii_uppercase() }@@ -320,7 +338,8 @@ fn bjt_check(pins: &[&Pin], shapes: &[&Shape]) -> Option<(Sev, String)> { fn opamp_check(sym: &LibSym, pins: &[&Pin], texts: &[&Txt], custom: bool) -> Vec<(Sev, String)> { let mut out = Vec::new();- let inputs: Vec<(&Pin, char)> = pins.iter().filter_map(|p| amp_input(&p.name).map(|s| (*p, s))).collect();+ if two_terminal(sym) { return out; }+ let inputs: Vec<(&Pin, char)> = pins.iter().filter_map(|p| amp_input_on(sym, &p.name).map(|s| (*p, s))).collect(); let signs: Vec<(&Txt, char)> = texts.iter().filter_map(|t| match norm(&t.text).as_str() { "+" => Some((*t, '+')), "-" => Some((*t, '-')), _ => None }).collect(); if signs.is_empty() { if !inputs.is_empty() && sym.names_hidden { out.push((Sev::Warn, "the input pins are named +/- but pin names are hidden and no +/- is drawn: the inputs show no polarity".into())); }@@ -378,7 +397,7 @@ fn pin_types(sym: &LibSym) -> Vec<(Sev, Pin, String)> { let et = p.etype.as_str(); // a stacked pin (same unit, same place as another pin) is passive by KiCad's own convention if et == "passive" && sym.pins.iter().any(|q| !std::ptr::eq(q, p) && q.unit == p.unit && close(q.at, p.at) && q.etype != "passive") { continue; }- if let Some(s) = amp_input(&p.name) {+ if let Some(s) = amp_input_on(sym, &p.name) { if et == "passive" { out.push((Sev::Error, p.clone(), format!("amplifier input `{}` ({s}) is passive; declare it input", p.name))); } else if et != "input" { out.push((Sev::Warn, p.clone(), format!("amplifier input `{}` is {et}; amplifier inputs are input", p.name))); } } else if is_output(&p.name) {@@ -786,6 +805,37 @@ mod tests { (pin passive line (at 0 -7.62 90) (length 2.54) (name "V-" (effects (font (size 1.27 1.27)))) (number "4" (effects (font (size 1.27 1.27)))))))"#) } + /// A symbol of passive pins, as (name, number).+ fn plain_lib(name: &str, pins: &[(&str, &str)]) -> String {+ let rows: Vec<String> = pins.iter().enumerate().map(|(i, (n, num))| format!(r#"(pin passive line (at {} 0 0) (length 2.54) (name "{n}" (effects (font (size 1.27 1.27)))) (number "{num}" (effects (font (size 1.27 1.27)))))"#, i as f64 * 5.08)).collect();+ format!(r#"(symbol "{name}" (property "Reference" "C" (at 0 5 0) (effects (font (size 1.27 1.27)))) (symbol "P_1_1" (text "+" (at 1 3 0) (effects (font (size 1.27 1.27)))) {}))"#, rows.join(" "))+ }+ fn errors_of(lib: &str, sym: &str, r: &str) -> Vec<String> {+ let f = Fixture::new();+ let p = f.write("s.kicad_sch", &sheet(&[lib.to_string()], &placed(sym, r, 120.0, 90.0)));+ let rep = check(&p, &Value::Null).unwrap();+ rep["errors"].as_array().unwrap().iter().map(|e| e.as_str().unwrap().to_string()).collect()+ }+ #[test]+ fn terminals_named_plus_and_minus_are_not_amplifier_inputs() {+ // issue #126: an electrolytic with pins + and an en dash, both passive, and a two-pin IN+/IN- terminal+ for (sym, pins) in [("Trial:EEEFTH101XAP", [("+", "1"), ("\u{2013}", "2")]), ("Trial:TERM", [("IN+", "1"), ("IN-", "2")])] {+ let errs = errors_of(&plain_lib(sym, &pins), sym, "C7");+ assert!(errs.iter().all(|e| !e.contains("amplifier input")), "{errs:?}");+ }+ // a battery connector with a third pin and no output: bare +/- are still its terminals+ let errs = errors_of(&plain_lib("Trial:BATT", &[("+", "1"), ("-", "2"), ("NTC", "3")]), "Trial:BATT", "J1");+ assert!(errs.iter().all(|e| !e.contains("amplifier input")), "{errs:?}");+ }+ #[test]+ fn a_real_amplifier_with_bare_or_passive_inputs_still_errors() {+ // bare +/- beside an output is an amplifier+ let errs = errors_of(&plain_lib("Trial:AMP", &[("+", "1"), ("-", "2"), ("OUT", "3")]), "Trial:AMP", "U1");+ assert!(errs.iter().any(|e| e.contains("amplifier input")), "{errs:?}");+ // IN+ needs no output beside it to be an input+ let errs = errors_of(&plain_lib("Trial:SENSE", &[("IN+", "1"), ("IN-", "2"), ("REF", "3")]), "Trial:SENSE", "U2");+ assert!(errs.iter().any(|e| e.contains("amplifier input")), "{errs:?}");+ } #[test] fn passive_amplifier_and_power_pins_are_errors_unless_justified() { let f = Fixture::new();
docs/release-0.2.43.mdadded+8@@ -0,0 +1,8 @@+# AI Flow 0.2.43 (2026-11-02)++Schematic check (#126):++- **A polarized part's `+` and `-` pins are no longer called amplifier inputs.** A part with two terminals, such as an electrolytic capacitor, a battery or a polarized connector, has no amplifier inputs, so passive pins are right.+- **Bare `+` and `-` count as amplifier inputs only on something that looks like an amplifier:** an op amp or comparator by its description, or a symbol with an output pin beside them. So a `+ - NTC` battery connector passes too. A real amplifier with passive inputs is still an error, and `IN+` and `IN-` on a symbol with three or more pins still count without an output.++Checked with the real adom/eeefth101xap symbol through a real kicad-cli: 0.2.42 gives both errors, and this build gives neither.
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.2.42",+ "version": "0.2.43", "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.42",+ "version": "0.2.43", "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
Log in to comment.
No comments yet.