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
0.1.33: native zone refill in the gate, remote kicad-cli for containers, analyses on adopted boards, per-layer numbered Kelvin keepouts, thermal vias beside small pins; fab named generically in the docs
21 files changed
+313−72
Cargo.lock+10−10Cargo.toml+1−1README.md+3−3SKILL.md+2−2bin/adom-aiflowcrates/adom-aiflow/src/main.rs+39−3crates/aiflow-copper/src/lib.rs+30−8crates/aiflow-pours/src/lib.rs+134−27docs/release-0.1.33.md+11docs/silkscreen-priority-pass.md+1−1docs/silkscreen.md+5−5docs/spec-example.json+1−1flows/board.json+4−4install.sh+2package.json+1−1page.json+1−1skills/adom-aiflow/SKILL.md+2−2skills/aiflow-silkscreen/SKILL.md+1−1tools/kicad-cli-remote+63tools/silkscreen-dashboard/SKILL.md+1−1uninstall.sh+1−1Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.1.32"+version = "0.1.33" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -23,7 +23,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.1.32"+version = "0.1.33" dependencies = [ "serde", "serde_json",@@ -32,7 +32,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.1.32"+version = "0.1.33" dependencies = [ "serde", "serde_json",@@ -40,7 +40,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.1.32"+version = "0.1.33" dependencies = [ "serde", "serde_json",@@ -48,7 +48,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.1.32"+version = "0.1.33" dependencies = [ "aiflow-board", "aiflow-grid",@@ -58,7 +58,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.1.32"+version = "0.1.33" dependencies = [ "aiflow-board", "serde",@@ -67,7 +67,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.1.32"+version = "0.1.33" dependencies = [ "aiflow-board", "serde",@@ -76,7 +76,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.1.32"+version = "0.1.33" dependencies = [ "aiflow-board", "aiflow-copper",@@ -86,7 +86,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.1.32"+version = "0.1.33" dependencies = [ "aiflow-board", "aiflow-grid",@@ -96,7 +96,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.1.32"+version = "0.1.33" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.1.32"+version = "0.1.33" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
README.md+3−3@@ -127,13 +127,13 @@ Everything below is what John has asked for, in the order it is likely to land. **Steps after the board (the flow grows at the back)** - **moleculize**: add the machine pins so the board can live in the probing workcell as a molecule. - **paste**: solder paste jetting calculations and analysis.-- **probe**: work out how to probe the board when it comes off the InstaPCB process.+- **probe**: work out how to probe the board when it comes off the in-house fab's process. **Analyses (0.2)** - **current 0.2**: cross-sections through the filled copper instead of the narrowest-track heuristic; a current-density heat map per net. - **thermal 0.2**: copper area weighted by distance from the tab, via conduction, the other layer's contribution; a heat map per hot part. - **impedance**: controlled-impedance and return-path checks on the nets the spec marks.-- **ablation**: the copper-kept metric per layer as a mill-time estimate for the InstaPCB process, so "least copper to ablate" is a number the AI can push.+- **ablation**: the copper-kept metric per layer as a mill-time estimate for the in-house fab's process, so "least copper to ablate" is a number the AI can push. **The video (0.2)** - The step's own code frames the shot: zoom to fit before a landing, the part on screen when a move lands.@@ -172,7 +172,7 @@ Everything below is what John has asked for, in the order it is likely to land. A pour's area or presence is not proof of current capacity or acceptable temperature. `analyze current` and `analyze thermal` now require both the heuristic checks and a matching Adom Fields solve. Run `adom-fields analyze --board <current saved board> --spec <run spec> --out <run>/fields`, then `analyze current --fields <run>/fields/fields.json` and `analyze thermal --fields <run>/fields/fields.json`. Fields records SHA-256 identities for the exact inputs. Changed boards or specs invalidate the result; unsolved loaded nets or hot parts are not a pass. `finish` rechecks the evidence. This is an engineering screen under the solver's documented assumptions, not junction-temperature certification. -Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. Manufacturing-rule warnings, including hole spacing, remain blocking.+Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. In a container without native KiCad, `ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote` (installed with AI Flow) runs `pcb drc` on a desktop's KiCad through Adom Bridge (`KICAD_REMOTE_TARGET`, default ConfRoomROG); a stock distro kicad-cli that lacks `pcb drc --refill-zones` is ignored. The gate refills zones natively when it has a native kicad-cli, so zone fills are KiCad's own, not the raster estimate, and the current and thermal analyses read the board's own copper when the routing plan leaves nets out (an adopted, already-routed board). Manufacturing-rule warnings, including hole spacing, remain blocking. Latest maintenance: [0.1.28 lifecycle and native IPC readiness](docs/release-0.1.28.md).
SKILL.md+2−2@@ -51,7 +51,7 @@ No recording may outlive an hour (a hard cap on every recording), `finish` and ` ## The spec -`docs/spec-example.json` on the page is the ESC G431's: copper thickness, clearances, the inherited error count, fixed refs (the molecule interface), planes, wide and mid nets, Kelvin pairs, loads per net (amps, max rise), hot parts (watts, tab net), the pours (outline, around parts with a margin and exclusions, or explicit polygons; priorities and connection styles), solid patches, thermal and stitching vias, and the nets that are deliberately not poured. Write the spec from the schematic before you start; it is the electrical judgement, and it is what makes two engines' runs comparable.+`docs/spec-example.json` on the page is the ESC G431's: copper thickness, clearances, the inherited error count, fixed refs (the molecule interface), planes, wide and mid nets, Kelvin pairs, loads per net (amps, max rise), hot parts (watts, tab net), the pours (outline, around parts with a margin and exclusions, or explicit polygons; priorities and connection styles), solid patches, thermal and stitching vias, and the nets that are deliberately not poured. A `thermalVias` entry on a pin too small to hold a via (SOT-23, 0603) gets 0.6/0.3 mm vias in the pour just outside the pad, clear of other nets, the stitch vias and the vias already on the board, never one in the pad (an open via in a small pad wicks the solder); `pour` says how many of `count` fit. Kelvin tap keepouts follow the tap's own layer (both layers only at its vias), stop short of the power pin the tap lands on so that pin keeps its pour, and are numbered per net. Write the spec from the schematic before you start; it is the electrical judgement, and it is what makes two engines' runs comparable. ## The rules behind the stages @@ -155,7 +155,7 @@ Custom manual stages default to AI. `take custom-stage=ai` or `take custom-stage A pour's area or presence is not proof of current capacity or acceptable temperature. `analyze current` and `analyze thermal` now require both the heuristic checks and a matching Adom Fields solve. Run `adom-fields analyze --board <current saved board> --spec <run spec> --out <run>/fields`, then `analyze current --fields <run>/fields/fields.json` and `analyze thermal --fields <run>/fields/fields.json`. Fields records SHA-256 identities for the exact inputs. Changed boards or specs invalidate the result; unsolved loaded nets or hot parts are not a pass. `finish` rechecks the evidence. This is an engineering screen under the solver's documented assumptions, not junction-temperature certification. -Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. Manufacturing-rule warnings, including hole spacing, remain blocking.+Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. In a container without native KiCad, `ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote` (installed with AI Flow) runs `pcb drc` on a desktop's KiCad through Adom Bridge (`KICAD_REMOTE_TARGET`, default ConfRoomROG); a stock distro kicad-cli that lacks `pcb drc --refill-zones` is ignored. The gate refills zones natively when it has a native kicad-cli, so zone fills are KiCad's own, not the raster estimate, and the current and thermal analyses read the board's own copper when the routing plan leaves nets out (an adopted, already-routed board). Manufacturing-rule warnings, including hole spacing, remain blocking. Component library footage: keep the full `library-tour` separate. A reviewed five-second `library-overview` artifact may enter compose, with `--review` binding the exact selected STEP/GLB variants, current board, clip and native readback/screenshot. See [the review contract](https://wiki.adom.inc/adom/adom-aiflow/files/docs/library-overview-review.md). Never claim native model matching from a tour alone.
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+39−3@@ -177,6 +177,42 @@ fn spec_of(r: &Run) -> Value { read_json(r.data["spec"].as_str().unwrap_or("")) } +/// The board's own copper as plan-shaped entries (one per net and track width, one per via), so the+/// analyses see a board whose copper is already on it (an adopted, hand-reworked or natively routed+/// board) and not only what a routing plan adds.+fn board_copper_plan(b: &Board) -> Value {+ let net_of = |n: &aiflow_board::sx::Node| -> String {+ let Some(x) = n.find("net") else { return String::new() };+ match x.number(1) { Some(v) => b.net_table.get(&(v as i64)).cloned().unwrap_or_default(), None => x.atom(1).unwrap_or("").to_string() }+ };+ let mut entries: Vec<Value> = Vec::new();+ let mut seen: std::collections::BTreeSet<(String, String)> = std::collections::BTreeSet::new();+ for s in b.root.find_all("segment") {+ let (net, w) = (net_of(s), s.find("width").and_then(|w| w.number(1)).unwrap_or(0.0));+ if net.is_empty() || w <= 0.0 { continue; }+ if seen.insert((net.clone(), format!("{w:.4}"))) { entries.push(json!({"net": net, "width": w, "source": "board"})); }+ }+ for v in b.root.find_all("via") {+ let net = net_of(v);+ if net.is_empty() { continue; }+ let at = v.find("at");+ let (x, y) = (at.and_then(|a| a.number(1)).unwrap_or(0.0), at.and_then(|a| a.number(2)).unwrap_or(0.0));+ let drill = v.find("drill").and_then(|d| d.number(1)).unwrap_or(0.3);+ entries.push(json!({"net": net, "viaDrill": drill, "paths": [[{"layer": "B.Cu", "x": x, "y": y}]], "source": "board"}));+ }+ json!({"nets": entries})+}++/// The plan, plus the board's own copper for every net the plan carries no copper for.+fn plan_with_board_copper(plan: &Value, b: &Board) -> Value {+ let mut entries: Vec<Value> = plan.get("nets").and_then(|n| n.as_array()).cloned().unwrap_or_default();+ let planned: std::collections::BTreeSet<String> = entries.iter().filter_map(|e| e.get("net").and_then(|n| n.as_str()).map(str::to_string)).collect();+ for e in board_copper_plan(b)["nets"].as_array().cloned().unwrap_or_default() {+ if !planned.contains(e["net"].as_str().unwrap_or("")) { entries.push(e); }+ }+ json!({"nets": entries})+}+ fn pad_layers_of(b: &Board) -> BTreeMap<String, Vec<String>> { let mut m: BTreeMap<String, Vec<String>> = BTreeMap::new(); for p in &b.pads {@@ -1828,7 +1864,7 @@ fn main() { let b = board_of(&r); let plan_v = read_json(&planp); r.stage_start("gate", Some("binary"), None);- let (text, stats) = aiflow_copper::apply(&b, &plan_v, true).unwrap_or_else(|e| err(&format!("plan does not apply to the board: {e}"), &[]));+ let (text, stats) = aiflow_copper::apply(&b, &plan_v, !aiflow_copper::native_drc_available()).unwrap_or_else(|e| err(&format!("plan does not apply to the board: {e}"), &[])); let routed = dir.join("board-routed.kicad_pcb"); std::fs::write(&routed, &text).unwrap(); aiflow_copper::copy_project_rules(&r.current_board(), &routed.display().to_string()).unwrap_or_else(|e|err(&e,&[]));@@ -2008,7 +2044,7 @@ fn main() { thread(&cli); let mut r = load_run(&cli); let planp = plan.clone().or_else(|| r.outcomes().get("route").and_then(|o| o.get("plan")).and_then(|p| p.as_str()).map(str::to_string)).unwrap_or_else(|| err("no routing plan", &["Run route, or pass --plan.".into()]));- let plan_v = read_json(&planp);+ let plan_v = plan_with_board_copper(&read_json(&planp), &board_of(&r)); let spec = spec_of(&r); let zsp = dir.join("zone-state.json"); let zs = if zsp.is_file() { Some(read_json(&zsp.display().to_string())) } else { None };@@ -2041,7 +2077,7 @@ fn main() { let fields_hint = "Adom Fields 0.4.0 also supports specified conducting heatsinks, per-layer copper thickness, and an optional Signals view for overlap capacitance and differential-pair screening. Read its heatsinks-and-signals guide; use real assembly/material data, never invent a heatsink to pass this gate. Unsupported signal geometry is not a passing impedance result. adom-fields does this solve for you and shows it on the board in 3D: `adom-fields analyze --board <current board> --spec <spec> --out fields` (about two minutes), `adom-fields serve --fields fields &`, then under the fields step `tour fields` films the walkthrough; its screenshots are drawings you can register with `artifact`.".to_string(); let walk = if pour_nets.is_empty() { String::new() } else { format!("Then walk the pours this analysis judged on camera, under this same step: `tour nets --nets {}` (each net lit as a whole, pours and traces together, framed).", pour_nets.join(",")) }; if !passed {- err(&format!("{what} analysis not passed: {field_note}\n{}", lines.join("\n")), &["Fix what the failing lines say (pours, vias, widths, or placement), land it, measure again, then analyze again.".into(), draw, walk]);+ err(&format!("{what} analysis not passed:\n{}\n(field screen: {field_note})", lines.join("\n")), &["Fix what the failing lines say (pours, vias, widths, or placement), land it, measure again, then analyze again.".into(), draw, walk]); } ok(&format!("{what} analysis passed:\n{}", lines.join("\n")), &[draw, fields_hint, walk, "The heuristic and same-input Fields solve are engineering screens with documented assumptions, not manufacturing or thermal qualification.".into()]); }
crates/aiflow-copper/src/lib.rs+30−8@@ -1,8 +1,10 @@ //! Plans to copper on a board file, and KiCad's DRC on the result. //! //! `apply` writes a routing plan (segments and vias as `kicad_route_net` would land them) into a-//! copy of the board, and by default fills every zone with a raster fill so the shared headless-//! DRC (`service-kicad pcb drc`, which does not refill zones) counts plane vias as connected.+//! copy of the board. With a native KiCad (kicad-cli 9+) the DRC refills every zone itself+//! (`--refill-zones`), so the pours are KiCad's own fill; only for the shared headless DRC+//! (`service-kicad pcb drc`, which does not refill zones) does `apply` add a raster fill, so plane+//! vias count as connected there. //! `run_drc` and `summarize` give the gate its numbers: errors, new versus inherited against a //! baseline, unconnected items. use std::collections::{BTreeMap, HashMap};@@ -449,18 +451,38 @@ pub fn copy_project_rules(source: &str, destination: &str) -> Result<(), String> Ok(()) } +/// The native kicad-cli to use for DRC, when one is usable: `ADOM_AIFLOW_KICAD_CLI` or `kicad-cli`,+/// and only if its `pcb drc` supports `--refill-zones` (KiCad 9+). A stock KiCad 7 `kicad-cli` answers+/// `--version` but has no `pcb drc` and cannot read a KiCad 10 board, so it is not native here.+/// Cached: the probe spawns processes (and may cross Adom Bridge to a desktop).+pub fn native_kicad_cli() -> Option<String> {+ static NATIVE: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();+ NATIVE.get_or_init(|| {+ let cli = std::env::var("ADOM_AIFLOW_KICAD_CLI").unwrap_or_else(|_| "kicad-cli".into());+ let help = Command::new(&cli).args(["pcb", "drc", "--help"]).output().ok()?;+ let text = format!("{}{}", String::from_utf8_lossy(&help.stdout), String::from_utf8_lossy(&help.stderr));+ text.contains("--refill-zones").then_some(cli)+ }).clone()+}++/// True when DRC runs on a native KiCad that refills zones itself (so no raster fill is needed).+pub fn native_drc_available() -> bool {+ native_kicad_cli().is_some()+}+ pub fn run_drc(path: &str) -> Result<Value, String> { let dir = std::env::temp_dir().join(format!("aiflow-drc-{}", std::process::id())); std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; let report = dir.join(format!("drc-{}.json", uuid()));- let native = std::env::var("ADOM_AIFLOW_KICAD_CLI").unwrap_or_else(|_| "kicad-cli".into());- let has_native = Command::new(&native).arg("--version").output().map(|o|o.status.success()).unwrap_or(false);+ let native = native_kicad_cli(); let has_rules = ["kicad_pro", "kicad_dru"].iter().any(|ext|std::path::Path::new(path).with_extension(ext).is_file());- if has_rules && !has_native {- return Err("project rules are present but service-kicad accepts only the PCB and would lose them. Set ADOM_AIFLOW_KICAD_CLI to a native kicad-cli executable and retry this command; do not strip the project files to obtain a pass".into());+ if has_rules && native.is_none() {+ return Err("project rules are present but no native kicad-cli with `pcb drc --refill-zones` was found (a stock KiCad 7 kicad-cli does not count), and service-kicad accepts only the PCB and would lose the rules. Set ADOM_AIFLOW_KICAD_CLI to a KiCad 9+ kicad-cli (in a container with no native KiCad: ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote runs a desktop's KiCad through Adom Bridge; KICAD_REMOTE_TARGET picks the desktop) and retry; do not strip the project files to obtain a pass".into()); }- let out = if has_native {- Command::new(&native).args(["pcb","drc","--format","json","--output",report.to_str().unwrap(),path]).output()+ // Native KiCad refills every zone itself, so the copper in the pours is KiCad's own fill,+ // current for whatever the board now holds. The raster fill stays only for the headless service.+ let out = if let Some(cli) = &native {+ Command::new(cli).args(["pcb","drc","--refill-zones","--format","json","--output",report.to_str().unwrap(),path]).output() } else { Command::new("service-kicad").args(["pcb", "drc", "--format", "json", "--out", report.to_str().unwrap(), path]).output() }.map_err(|e|format!("DRC backend not runnable: {e}"))?;
crates/aiflow-pours/src/lib.rs+134−27@@ -57,6 +57,24 @@ fn plan_blockers(plan: &Value) -> Vec<(Pt, Pt, String, f64)> { out } +/// `p` moved along the segment toward `other` until it is `stop` away from `c` (or onto `other`).+fn pull_back(p: Pt, other: Pt, c: Pt, stop: f64) -> Pt {+ let (dx, dy) = (other.0 - p.0, other.1 - p.1);+ let n = dx.hypot(dy);+ if n < 1e-9 {+ return p;+ }+ let mut t = 0.0;+ while t < n {+ t = (t + 0.02).min(n);+ let q = (p.0 + dx * t / n, p.1 + dy * t / n);+ if (q.0 - c.0).hypot(q.1 - c.1) >= stop {+ return q;+ }+ }+ other+}+ fn clear(x: f64, y: f64, net: &str, blockers: &[(Pt, Pt, String, f64)]) -> bool { !blockers.iter().any(|(a, b, n, need)| n != net && geom::seg_distance(x, y, a.0, a.1, b.0, b.1) < *need) }@@ -124,20 +142,74 @@ pub fn build(board: &Board, spec: &Value, plan: Option<&Value>) -> Result<(Vec<V let n = tv.get("count").and_then(|c| c.as_u64()).unwrap_or(2) as usize; let (size, drill) = (tv.get("size").and_then(|v| v.as_f64()).unwrap_or(0.8), tv.get("drill").and_then(|v| v.as_f64()).unwrap_or(0.4)); let mut placed = 0;- for (dx, dy) in [(-1.2, 0.0), (1.2, 0.0), (-1.2, -1.3), (1.2, -1.3), (-1.2, 1.3), (1.2, 1.3), (0.0, -1.3), (0.0, 1.3), (0.0, 0.0)] {- if placed >= n {- break;+ let fits_inside = p.half_long - 0.4 > 0.0 && p.half_short - 0.4 > 0.0 && size <= 2.0 * p.half_short;+ if fits_inside {+ for (dx, dy) in [(-1.2, 0.0), (1.2, 0.0), (-1.2, -1.3), (1.2, -1.3), (-1.2, 1.3), (1.2, 1.3), (0.0, -1.3), (0.0, 1.3), (0.0, 0.0)] {+ if placed >= n {+ break;+ }+ let inside = (dx as f64).abs() < p.half_long - 0.4 && (dy as f64).abs() < p.half_short - 0.4;+ if !inside && (dx, dy) != (0.0, 0.0) {+ continue;+ }+ let (x, y) = (((p.x + dx) * 100.0).round() / 100.0, ((p.y + dy) * 100.0).round() / 100.0);+ if clear(x, y, &net, &blockers) {+ vias.push(via_entry(&net, x, y, size, drill));+ placed += 1;+ } }- let inside = (dx as f64).abs() < p.half_long - 0.4 && (dy as f64).abs() < p.half_short - 0.4;- if !inside && (dx, dy) != (0.0, 0.0) {- continue;+ } else {+ // a small pin (SOT-23, 0603...) has no room for a via: an open via in the pad wicks the+ // solder away. Put small vias in the pour just outside the pad instead, on a ring of+ // candidates nearest first, clear of other nets' pads and planned copper and of each other.+ let (size, drill) = (tv.get("size").and_then(|v| v.as_f64()).unwrap_or(0.6).min(0.6), tv.get("drill").and_then(|v| v.as_f64()).unwrap_or(0.3).min(0.3));+ let vr = size / 2.0;+ let mut cands: Vec<Pt> = Vec::new();+ for ring in 0..4 {+ let off = 0.25 + vr + ring as f64 * 0.35;+ for k in 0..16 {+ let t = k as f64 * std::f64::consts::PI / 8.0;+ let (ex, ey) = (t.cos(), t.sin());+ // edge of the pad's box along this direction, in board axes+ let (hx, hy) = if p.long_axis.0.abs() >= p.long_axis.1.abs() { (p.half_long, p.half_short) } else { (p.half_short, p.half_long) };+ let reach = if ex.abs() * hy > ey.abs() * hx { hx / ex.abs().max(1e-9) } else { hy / ey.abs().max(1e-9) };+ cands.push((p.x + ex * (reach + off), p.y + ey * (reach + off)));+ } }- let (x, y) = (((p.x + dx) * 100.0).round() / 100.0, ((p.y + dy) * 100.0).round() / 100.0);- if clear(x, y, &net, &blockers) {- vias.push(via_entry(&net, x, y, size, drill));- placed += 1;+ let (bx0, by0, bx1, by1) = board.bbox;+ // keep clear of the spec's stitch vias and any via already on the board, not only our own+ let mut mine: Vec<Pt> = spec.get("stitchVias").and_then(|v| v.as_array()).cloned().unwrap_or_default().iter()+ .filter_map(|v| v.get("x").and_then(|x| x.as_f64()).zip(v.get("y").and_then(|y| y.as_f64()))).collect();+ mine.extend(board.root.find_all("via").filter_map(|v| { let at = v.find("at")?; Some((at.number(1)?, at.number(2)?)) }));+ for (cx, cy) in cands {+ if placed >= n {+ break;+ }+ let (x, y) = ((cx * 100.0).round() / 100.0, (cy * 100.0).round() / 100.0);+ if x - vr < bx0 + edge || x + vr > bx1 - edge || y - vr < by0 + edge || y + vr > by1 - edge {+ continue;+ }+ let near_other_pad = board.pads.iter().any(|q| q.net != net && q.polys.iter().flatten().count() > 0 && {+ let (qx0, qy0, qx1, qy1) = geom::bbox(&q.polys.iter().flatten().copied().collect::<Vec<_>>());+ let (nx, ny) = (x.clamp(qx0, qx1), y.clamp(qy0, qy1));+ (x - nx).hypot(y - ny) < vr + clearance + 0.1+ });+ let near_own_pad = board.pads.iter().any(|q| q.polys.iter().flatten().count() > 0 && {+ let (qx0, qy0, qx1, qy1) = geom::bbox(&q.polys.iter().flatten().copied().collect::<Vec<_>>());+ let (nx, ny) = (x.clamp(qx0, qx1), y.clamp(qy0, qy1));+ (x - nx).hypot(y - ny) < vr + 0.1+ });+ let near_mine = mine.iter().any(|m| (m.0 - x).hypot(m.1 - y) < size + 0.3);+ if !near_other_pad && !near_own_pad && !near_mine && clear(x, y, &net, &blockers) {+ vias.push(via_entry(&net, x, y, size, drill));+ mine.push((x, y));+ placed += 1;+ } } }+ if placed < n {+ eprintln!("thermal vias at {key}: placed {placed} of {n}; no more room clear of other nets (add stitchVias by hand, or accept fewer)");+ } } for sv in spec.get("stitchVias").and_then(|v| v.as_array()).cloned().unwrap_or_default() { let (Some(net), Some(x), Some(y)) = (sv.get("net").and_then(|n| n.as_str()), sv.get("x").and_then(|v| v.as_f64()), sv.get("y").and_then(|v| v.as_f64())) else { continue };@@ -157,6 +229,10 @@ pub fn build(board: &Board, spec: &Value, plan: Option<&Value>) -> Result<(Vec<V keepouts += 1; } }+ // the power pins the taps land on (spec.kelvin values), with the radius a keepout must stay out of+ let kelvin_power_pads: Vec<(f64, f64, f64)> = spec.get("kelvin").and_then(|k| k.as_object()).map(|m| m.values().filter_map(|v| v.as_str()).filter_map(|k| board.pad_by_key(k)).map(|p| (p.x, p.y, p.half_long.max(p.half_short))).collect()).unwrap_or_default();+ // every keepout gets a unique name: KiCad Bridge's zone plan refuses duplicates+ let mut tap_count: std::collections::HashMap<String, usize> = std::collections::HashMap::new(); if let (Some(plan), true) = (plan, spec.get("kelvin").map(|k| !k.is_null()).unwrap_or(false)) { for e in plan.get("nets").and_then(|n| n.as_array()).cloned().unwrap_or_default() { if !e.get("kelvin").and_then(|k| k.as_bool()).unwrap_or(false) {@@ -164,25 +240,56 @@ pub fn build(board: &Board, spec: &Value, plan: Option<&Value>) -> Result<(Vec<V } let net = e.get("net").and_then(|n| n.as_str()).unwrap_or("").to_string(); let w = e.get("width").and_then(|w| w.as_f64()).unwrap_or(0.25) / 2.0 + 0.2 + 0.1;+ let via_r = e.get("viaSize").and_then(|v| v.as_f64()).unwrap_or(0.8) / 2.0 + 0.2 + 0.1;+ let r = |v: f64| (v * 1000.0).round() / 1000.0; for path in e.get("paths").and_then(|p| p.as_array()).cloned().unwrap_or_default() {- let pts: Vec<Pt> = path.as_array().cloned().unwrap_or_default().iter().filter_map(|wp| {- if let Some(a) = wp.as_array() { if a.len() == 2 { return Some((a[0].as_f64()?, a[1].as_f64()?)); } }- if let Some(o) = wp.as_object() { return Some((o.get("x")?.as_f64()?, o.get("y")?.as_f64()?)); }- if let Some(k) = wp.as_str() { let p = board.pad_by_key(k)?; return Some((p.x, p.y)); }- None- }).collect();- for pair in pts.windows(2) {- let ((ax, ay), (bx, by)) = (pair[0], pair[1]);- let (dx, dy) = (bx - ax, by - ay);- let n = dx.hypot(dy);- if n < 0.05 {- continue;+ // walk the path on its own layers: it starts on its first pad's layer, and a+ // {x, y, layer} waypoint is a via onto that layer. A keepout goes on the layer its+ // sense segment runs on (not both), so the other layer's plane stays whole.+ let wps = path.as_array().cloned().unwrap_or_default();+ let mut layer = wps.first().and_then(|w| w.as_str()).and_then(|k| board.pad_by_key(k)).map(|p| if p.on_f { "F.Cu" } else { "B.Cu" }).unwrap_or("F.Cu").to_string();+ let mut prev: Option<Pt> = None;+ for wp in &wps {+ let (pt, via_to) = if let Some(a) = wp.as_array() {+ (if a.len() == 2 { a[0].as_f64().zip(a[1].as_f64()) } else { None }, None)+ } else if let Some(o) = wp.as_object() {+ (o.get("x").and_then(|v| v.as_f64()).zip(o.get("y").and_then(|v| v.as_f64())), o.get("layer").and_then(|l| l.as_str()).map(str::to_string))+ } else if let Some(k) = wp.as_str() {+ (board.pad_by_key(k).map(|p| (p.x, p.y)), None)+ } else {+ (None, None)+ };+ let Some(b) = pt else { continue };+ if let Some(a) = prev {+ // stop short of the power pin the tap lands on: that pin must stay joined to+ // its pour (it is usually the part's heat path), only the tap is fenced+ let (mut a, mut bb) = (a, b);+ for d in &kelvin_power_pads {+ let stop = d.2 + clearance + 0.3;+ if (a.0 - d.0).hypot(a.1 - d.1) < stop { a = pull_back(a, bb, (d.0, d.1), stop); }+ if (bb.0 - d.0).hypot(bb.1 - d.1) < stop { bb = pull_back(bb, a, (d.0, d.1), stop); }+ }+ let ((ax, ay), (bx, by)) = (a, bb);+ let (dx, dy) = (bx - ax, by - ay);+ let n = dx.hypot(dy);+ if n >= 0.05 {+ let (ux, uy) = (dx / n, dy / n);+ let poly = json!([[r(ax - uy * w), r(ay + ux * w)], [r(bx - uy * w), r(by + ux * w)], [r(bx + uy * w), r(by - ux * w)], [r(ax + uy * w), r(ay - ux * w)]]);+ let k = tap_count.entry(net.clone()).or_insert(0);+ *k += 1;+ zones.push(json!({"type": "keepout", "layers": [layer.clone()], "polygon": poly, "keepout": {"copperPour": true}, "name": format!("Kelvin tap keepout {net} #{k}")}));+ keepouts += 1;+ }+ }+ if let Some(to) = via_to {+ // a via is copper on both layers: fence it on both, or a same-net plane joins the tap there+ let k = tap_count.entry(net.clone()).or_insert(0);+ *k += 1;+ zones.push(json!({"type": "keepout", "layers": ["F.Cu", "B.Cu"], "polygon": rect(b.0 - via_r, b.1 - via_r, b.0 + via_r, b.1 + via_r), "keepout": {"copperPour": true}, "name": format!("Kelvin tap keepout {net} #{k} (via)")}));+ keepouts += 1;+ layer = to; }- let (ux, uy) = (dx / n, dy / n);- let r = |v: f64| (v * 1000.0).round() / 1000.0;- let poly = json!([[r(ax - uy * w), r(ay + ux * w)], [r(bx - uy * w), r(by + ux * w)], [r(bx + uy * w), r(by - ux * w)], [r(ax + uy * w), r(ay - ux * w)]]);- zones.push(json!({"type": "keepout", "layers": ["F.Cu", "B.Cu"], "polygon": poly, "keepout": {"copperPour": true}, "name": format!("Kelvin tap keepout {net}")}));- keepouts += 1;+ prev = Some(b); } } }
docs/release-0.1.33.mdadded+11@@ -0,0 +1,11 @@+# AI Flow 0.1.33++Fixes found while building the second board with AI Flow (a 12 V to 5 V, 1 A buck molecule, KiCad 10, gate run from a container through a desktop's KiCad).++- **Gate: native zone refill.** With a native kicad-cli, `run_drc` now runs `pcb drc --refill-zones`, and the gate no longer adds the raster zone-fill estimate on top of it. On the buck the raster fill reported 242 phantom errors; the native refill gives 0. A stock distro kicad-cli without `pcb drc --refill-zones` (Ubuntu's KiCad 7) is no longer picked up.+- **Remote KiCad for containers.** `adom-aiflow-kicad-cli-remote` (installed with the package) implements `pcb drc` on a desktop's KiCad through Adom Bridge; set `ADOM_AIFLOW_KICAD_CLI` to it. The board travels with its `.kicad_pro` and `.kicad_dru`. `KICAD_REMOTE_TARGET` picks the desktop.+- **Analyses on an adopted board.** `analyze current` and `analyze thermal` merge the board's own tracks and vias into the routing plan, so a board adopted fully routed (empty plan) is analysed on its real copper instead of failing with `no-copper`. The failure line now lists every issue and the field screen's note.+- **Kelvin keepouts.** Tap keepouts sit on the layer the sense segment runs on (both layers only at the tap's vias), stop short of the power pin the tap lands on, so that pin keeps its pour and heat path, and are numbered per net (KiCad Bridge refused the duplicate names).+- **Thermal vias on small pins.** A pin too small to hold a via gets 0.6/0.3 mm vias in the pour just outside the pad, clear of other nets, the stitch vias and the board's vias; `pour` reports how many of `count` fit. The old fallback put one 0.8 mm via in the middle of a SOT-23 pad.++Acceptance: workspace tests pass; the gate, land route, land pours, measure, analyze current, analyze thermal and finish all passed on the buck board with this build.
docs/silkscreen-priority-pass.md+1−1@@ -9,7 +9,7 @@ The order below is explicit: a lower priority cannot buy a collision, incorrect 3. **Board-purpose importance.** Before placement, classify the board and rank its human interfaces. On ESCs, prominently label PHASE A/B/C and battery polarity/ground. Other boards get their own appropriate prominent interfaces, supply limits and critical warnings. Functional terminal names may outrank MC/J references. Use only sourced descriptions/limits. 4. **Locality and association.** Keep each name/value pair atomic and close to its component. For edge contacts prefer the space between contact and board edge, consistently across a row; use inside placement only when needed. Pin labels face their target: right-aligned left of the pad, left-aligned right of it, reversed appropriately for a readable bottom-face view. Use curved pointers when association is unclear. 5. **Group design.** Repeated contact frames default on, with a user-off option. Solve common left/right rails, width/height, padding and rhythm for a row/column. Do not stretch a frame after checking collisions. Clearance gaps and exceptional font/inside placements are explicit. Calculation bounds are dashed and translucent; only actual manufacturing text/pointers/frames are opaque solid strokes.-6. **Readable typography.** Maximize font size within the above constraints. Start ordinary references around 0.8 mm and values around 0.4 mm, with values smaller than references. Important interfaces often justify 1.0–1.4 mm or more where space permits. The user-authorized InstaPCB 0.3/0.2 mm value text is a last resort, not a default or a general fabrication guarantee. Document the selected sizes and any tiny-text compromise.+6. **Readable typography.** Maximize font size within the above constraints. Start ordinary references around 0.8 mm and values around 0.4 mm, with values smaller than references. Important interfaces often justify 1.0–1.4 mm or more where space permits. The user-authorized in-house fab 0.3/0.2 mm value text is a last resort, not a default or a general fabrication guarantee. Document the selected sizes and any tiny-text compromise. 7. **Interior documentation pass.** Reserve readable open INTERIOR regions for a source-backed board description, identity/revision, service instructions and reference tables. Prefer an interior layout at a readable font over a slightly larger edge layout. Keep physical terminal labels local. Preserve semantic blocks, left-aligned visible text, heading hierarchy and ordered rows. Reflow or split a large table into coherent columns before pushing it to the rim. Slight, quantized row-spacing increases may avoid vias; do not move lines independently or reorder them. If the middle cannot fit safely, report the blocking geometry, attempted sizes and explicit edge fallback. Include what the board does, not just a project code. 8. **Independent verification and final visual pass.** Check every text and stroke ahead of the EDA, then native DRC and native text/geometry read-back. Inspect top and bottom in both the editor and its refreshed 3D viewer. Review left alignment, common rails, name/value hierarchy, pin-1/polarity, no text hidden by bodies, no holes under text, and whether documentation uses interior space sensibly. Repair the general rule when a native check reveals a defect. 9. **Evidence.** Replay all required groups and real rework with active text sizes. Isolate faces and show the bottom from below (or explicitly mirrored from above). Record 1920×1080; frame the complete active group independently of manual inspection camera movement. Include final native EDA proof. Inspect contact sheets and actual webview playback. Never describe the replay as recorded internal AI reasoning or omit unresolved decisions from the report.
docs/silkscreen.md+5−5@@ -1,13 +1,13 @@ # Two-sided service silkscreen -The `silkscreen` AI-owned stage runs after analysis/net review and before the final native 3D tour. Plan label space at placement time. Read the reusable [InstaPCB silkscreen skill](https://wiki.adom.inc/adom/instapcb/files/skills/instapcb-silkscreen/SKILL.md).+The `silkscreen` AI-owned stage runs after analysis/net review and before the final native 3D tour. Plan label space at placement time. Read the in-house fab's silkscreen skill (internal) when building for it. Treat silkscreen as the board's built-in service manual. Add useful information generously, with a visual hierarchy and space between labels. Do not fill space with ambiguous or unreadable text. ## Process profile and provenance -For the InstaPCB profile requested by Adam (Adom CEO, 2026-09-16), use approximately 0.8 mm reference designators and 0.5 mm secondary value text where space allows. He reports that InstaPCB's UV fiber laser process can render readable 0.5 mm text. This is a named process target, not a universal fab minimum or a measured acceptance result. Verify the current station profile for stroke width, contrast, mask registration and clearances; retain the profile/version and inspect a physical coupon when fabrication qualification is required. Do not infer minimum stroke from text height. Preserve other fabs' rules and never disable DRC globally to force microtext through. Treat two-sided marking cost as a property of the selected service, not a universal free option.+For the in-house fab profile requested by Adam (Adom CEO, 2026-09-16), use approximately 0.8 mm reference designators and 0.5 mm secondary value text where space allows. He reports that the in-house fab's process can render readable 0.5 mm text. This is a named process target, not a universal fab minimum or a measured acceptance result. Verify the current station profile for stroke width, contrast, mask registration and clearances; retain the profile/version and inspect a physical coupon when fabrication qualification is required. Do not infer minimum stroke from text height. Preserve other fabs' rules and never disable DRC globally to force microtext through. Treat two-sided marking cost as a property of the selected service, not a universal free option. ## Plan before placement; finish after copper stabilizes @@ -16,8 +16,8 @@ For the InstaPCB profile requested by Adam (Adom CEO, 2026-09-16), use approxima 3. Label power inputs and returns, polarity, connector pin 1 and every accessible signal, machine pin/contact functions, programming/debug pinout, switch actions, LED meanings, test points and mounting orientation. Verify pin labels against actual numbered pads and nets, not the connector's apparent geometry. A net name does not establish a safe voltage or current rating. Print voltage range, maximum current and other limits only with approved design evidence; distinguish input rating, rail nominal voltage and absolute maximum. Every test point MUST have visible silkscreen identifying both its reference and verified net/signal or measurement function. Prioritize these labels before ordinary component values. Keep them adjacent to the accessible probe pad, or use a short unambiguous leader/key on the same accessible face when crowded. Check complete test-point coverage against the actual board; missing or ambiguous labels are unresolved findings, never silently omitted. Repeat on the opposite face when useful for mounted-board debugging, without implying a probe pad exists there. -4. Use approximately 1.2–2.0 mm for board identity and critical connection labels, 0.8 mm for references and 0.5 mm for values/secondary notes under the named InstaPCB profile. These are starting sizes, not mandatory packing rules. Prefer horizontal text and consistent reading directions; rotate to follow a connector only when that aids use. Use familiar engineering notation (10k, 100nF, 4.7uF); distinguish value, tolerance and voltage rating. Give every resistor/capacitor its reference plus a nearby value; search microtext placements before declaring a space constraint. Long IC MPNs may belong in a back-side key rather than in congested assembly space.-Keep each reference unmistakably associated with its own component. Prefer reducing reference font size locally over moving a label farther away. Aim for complete reference coverage; use a short clear leader only when proximity alone is ambiguous. Treat approximately 0.8 mm as an initial reference size, not a minimum. For dense InstaPCB artwork Adam explicitly permits secondary values at 0.3 mm or even 0.2 mm (2026-09-16); try 0.5, 0.3 then 0.2 mm while preserving the ref/value pairing and required stroke/spacing. These tiny sizes are user-requested artwork options, not independently verified laser-process capability. Keep the actual sizes and any unresolved physical legibility/DFM limits in the review; do not silently omit labels or globally weaken fab rules. Inspect the result at actual size and close-up.+4. Use approximately 1.2–2.0 mm for board identity and critical connection labels, 0.8 mm for references and 0.5 mm for values/secondary notes under the named in-house fab profile. These are starting sizes, not mandatory packing rules. Prefer horizontal text and consistent reading directions; rotate to follow a connector only when that aids use. Use familiar engineering notation (10k, 100nF, 4.7uF); distinguish value, tolerance and voltage rating. Give every resistor/capacitor its reference plus a nearby value; search microtext placements before declaring a space constraint. Long IC MPNs may belong in a back-side key rather than in congested assembly space.+Keep each reference unmistakably associated with its own component. Prefer reducing reference font size locally over moving a label farther away. Aim for complete reference coverage; use a short clear leader only when proximity alone is ambiguous. Treat approximately 0.8 mm as an initial reference size, not a minimum. For dense in-house fab artwork Adam explicitly permits secondary values at 0.3 mm or even 0.2 mm (2026-09-16); try 0.5, 0.3 then 0.2 mm while preserving the ref/value pairing and required stroke/spacing. These tiny sizes are user-requested artwork options, not independently verified laser-process capability. Keep the actual sizes and any unresolved physical legibility/DFM limits in the review; do not silently omit labels or globally weaken fab rules. Inspect the result at actual size and close-up. 5. For every machine pin, machine contact and edge-pin connector, repeat its reference/pin number and verified signal or power function on BOTH faces. A mounted board may expose only one side during debugging. Put the repeated labels beside the same physical connection where possible; when crowded, use a short clear leader to the actual connection on that face; a remote keyed legend is supplementary only. A pinout table on the other face alone does not satisfy this check. Review both faces in the mounted-access context, with bottom text correctly mirrored and pin numbering preserved. Use both F.SilkS and B.SilkS (or the EDA's native equivalents). Bottom text must read correctly when viewed from underneath, with the EDA's proper mirror setting; do not reverse the string. Put a clear pinout/service key on the less crowded face, mapped to reference and pad number. Copper/pour labels identify a verified net; avoid implying that hidden traces are visible or electrically isolated. 6. Protect exposed pads, solder-mask openings, test contacts, holes, board edges, fiducials, optical windows, component courtyards and mechanical interfaces. Consider visible space with components fitted: body footprints may obscure text even when DRC passes. Retain assembly-only markings on fabrication layers if useful, but do not count them as visible silkscreen. Never move copper or parts merely to force extra text without a recorded design return.@@ -52,7 +52,7 @@ Film real leader placement and ref/function resizing as part of progressive silk ## Complete local value coverage -For the requested InstaPCB profile, attempt a nearby value for EVERY resistor and capacitor, including rotated components and references. Search both orientations and adjacent sides at 0.5, 0.3 and 0.2 mm as needed, preserving an unmistakable reference/value association. Do not skip values merely because the reference is rotated, an initial placement fails, or a bottom table exists. Repack nearby silk or use a clear short leader when necessary. Audit actual-board value coverage and report each unresolved value explicitly; a back-side key is supplementary, not completion. Retain native mask/overlap checks and distinguish requested artwork sizes from measured physical legibility.+For the requested in-house fab profile, attempt a nearby value for EVERY resistor and capacitor, including rotated components and references. Search both orientations and adjacent sides at 0.5, 0.3 and 0.2 mm as needed, preserving an unmistakable reference/value association. Do not skip values merely because the reference is rotated, an initial placement fails, or a bottom table exists. Repack nearby silk or use a clear short leader when necessary. Audit actual-board value coverage and report each unresolved value explicitly; a back-side key is supplementary, not completion. Retain native mask/overlap checks and distinguish requested artwork sizes from measured physical legibility. ## Keep both native views current after every update
docs/spec-example.json+1−1@@ -1,6 +1,6 @@ { "board": "ESC G431 (Adom molecule: STM32G431, DRV8300, six BSC016N06NS, TPSM365 buck, INA181 current sense), 64 x 74 mm, 4 layers",- "fab": "InstaPCB (copper ablation: keep as much copper as the electrical rules allow)",+ "fab": "in-house fab (copper ablation: keep as much copper as the electrical rules allow)", "copperUm": 35, "clearance": 0.2, "edgeClearance": 0.5,
flows/board.json+4−4@@ -157,16 +157,16 @@ "who": "ai", "does": "Make both faces useful in real service: references and values, verified connector and machine-contact pinouts, polarity, board identity and bring-up labels; review native plots and assembled visibility. Offer the optional shared silkscreen dashboard: real solver events, candidate reasons, live/replay separation, fitted-model inspection and separately recorded detailed/5s replays. Audit every fitted body and every via/hole across the whole board, both faces, with coverage counts. Missing Fab geometry is not free space; native DRC does not establish fitted visibility. Keep unresolved labels explicit and do not apply an incomplete layout. Native geometry/edit/undo/refresh belongs to the EDA bridge.", "workflow": [- "Read the InstaPCB silkscreen skill for the selected process; reserve label space during placement and finalize after copper and analysis stabilize. Use both faces, a clear font-size hierarchy, and the approved process profile for small secondary text.",+ "Read the in-house fab's silkscreen skill for the selected process; reserve label space during placement and finalize after copper and analysis stabilize. Use both faces, a clear font-size hierarchy, and the approved process profile for small secondary text.", "Build a source-backed label manifest from actual schematic, pad numbers, nets and approved requirements. Include reference/value pairs, connector pinouts, power polarity, test points, switch/LED functions, revision and documentation link. Never infer voltage/current ratings from net names or component absolute maxima.", "For every machine pin, machine contact and edge-pin connector, repeat its reference/pin number and verified signal or power function on BOTH faces. A mounted board may expose only one side during debugging. Put the repeated labels beside the same physical connection where possible; when crowded, use a short clear leader to the actual connection on that face; a remote keyed legend is supplementary only. A pinout table on the other face alone does not satisfy this check. Review both faces in the mounted-access context, with bottom text correctly mirrored and pin numbering preserved.", "Every test point MUST have visible silkscreen identifying both its reference and verified net/signal or measurement function. Prioritize these labels before ordinary component values. Keep them adjacent to the accessible probe pad, or use a short unambiguous leader/key on the same accessible face when crowded. Check complete test-point coverage against the actual board; missing or ambiguous labels are unresolved findings, never silently omitted. Repeat on the opposite face when useful for mounted-board debugging, without implying a probe pad exists there.",- "Keep each reference unmistakably associated with its own component. Prefer reducing reference font size locally over moving a label farther away. Aim for complete reference coverage; use a short clear leader only when proximity alone is ambiguous. Treat approximately 0.8 mm as an initial reference size, not a minimum. For dense InstaPCB artwork Adam explicitly permits secondary values at 0.3 mm or even 0.2 mm (2026-09-16); try 0.5, 0.3 then 0.2 mm while preserving the ref/value pairing and required stroke/spacing. These tiny sizes are user-requested artwork options, not independently verified laser-process capability. Keep the actual sizes and any unresolved physical legibility/DFM limits in the review; do not silently omit labels or globally weaken fab rules. Inspect the result at actual size and close-up.",+ "Keep each reference unmistakably associated with its own component. Prefer reducing reference font size locally over moving a label farther away. Aim for complete reference coverage; use a short clear leader only when proximity alone is ambiguous. Treat approximately 0.8 mm as an initial reference size, not a minimum. For dense in-house fab artwork Adam explicitly permits secondary values at 0.3 mm or even 0.2 mm (2026-09-16); try 0.5, 0.3 then 0.2 mm while preserving the ref/value pairing and required stroke/spacing. These tiny sizes are user-requested artwork options, not independently verified laser-process capability. Keep the actual sizes and any unresolved physical legibility/DFM limits in the review; do not silently omit labels or globally weaken fab rules. Inspect the result at actual size and close-up.", "Avoid mask openings, contact surfaces, holes, fiducials and bodies that hide labels. Inspect bottom mirroring, actual-size legibility and both native 2D/3D faces. Use EDA bridge text/plot/DRC operations; give missing primitives back to the bridge.", "Run native DRC against the chosen fab profile and compare with the baseline. Preserve connectivity, placement, copper, outline and model transforms for silk-only edits. Register the label manifest and top/bottom evidence; return here when placements or pinouts change.", "Record BEFORE the first silkscreen mutation: show labels appearing individually or in small meaningful groups, pairing references with smaller values. Film actual moves, resizing, rotations and overlap corrections in order; preserve a timestamped operation/reason sidecar and raw uncaptioned footage. Use native bridge edits and refresh, never invented verbs. If native incremental editing is missing, file the bridge gap; identify any checkpoint reconstruction as a replay, never as original live placement. Keep a detailed action cut and use 3\u20135 seconds of accelerated population/rework in the final 120-second film. Read docs/silkscreen.md for recording and evidence rules.", "Require positional contact labels on BOTH faces: a pinout table is supplementary, never a substitute for text beside each actual machine contact, machine pin or edge connection. Separate primary reference (MC10) and smaller secondary function (DSHOT) as independently sized paired text. Prefer consistent reading directions. Where association remains ambiguous, add a short gentle curved silkscreen leader ending outside the intended pad mask opening; avoid crossings and obstacles. Verify one-to-one pad association, bottom mirroring, label/leader clearance and legibility in close-up native views and DRC. Film real additions and rework; retain mapping and unresolved constraints. See docs/silkscreen.md.",- "For the requested InstaPCB profile, attempt a nearby value for EVERY resistor and capacitor, including rotated components and references. Search both orientations and adjacent sides at 0.5, 0.3 and 0.2 mm as needed, preserving an unmistakable reference/value association. Do not skip values merely because the reference is rotated, an initial placement fails, or a bottom table exists. Repack nearby silk or use a clear short leader when necessary. Audit actual-board value coverage and report each unresolved value explicitly; a back-side key is supplementary, not completion. Retain native mask/overlap checks and distinguish requested artwork sizes from measured physical legibility.",+ "For the requested in-house fab profile, attempt a nearby value for EVERY resistor and capacitor, including rotated components and references. Search both orientations and adjacent sides at 0.5, 0.3 and 0.2 mm as needed, preserving an unmistakable reference/value association. Do not skip values merely because the reference is rotated, an initial placement fails, or a bottom table exists. Repack nearby silk or use a clear short leader when necessary. Audit actual-board value coverage and report each unresolved value explicitly; a back-side key is supplementary, not completion. Retain native mask/overlap checks and distinguish requested artwork sizes from measured physical legibility.", "After EVERY board or model update, refresh and verify BOTH the native 2D editor and its linked 3D viewer before showing/reporting completion. Check exact saved board revision and actual rendered changes in both windows. Reload cached models; if reopening is required preserve unsaved user work, close only task-owned stale windows and retain one current editor/viewer pair. Never overwrite a newer disk edit from a stale editor. Use native bridge controls and keep foreground preferences.", "Search space before shrinking text: 0.2 mm is a last resort. Use native text bounds and per-face body/pad/mask/via/silk obstacles, keep separate boxes for ref/value pairs, repack neighboring labels, preserve pin-row order and validate leader paths. Run silkscreen-layout on candidates; bounded search is not native verification. Apply by stable item ID, compare native DRC with baseline, inspect fitted visibility, and refresh BOTH native views. See docs/silkscreen.md." ],@@ -234,7 +234,7 @@ }, { "name": "probe",- "does": "how to probe the board when it comes off the InstaPCB process"+ "does": "how to probe the board when it comes off the in-house fab's process" } ], "clips": "every `step <name>` stops the previous step's clip and starts a new window recording tagged with the step, when the board is open on a test box; run.json captures[] carries one entry per clip with its step, start, stop and file, and deliver lists them; the final video is cut from these clips, one segment per step, so two engines' videos line up step for step; a return (step <name> --back --why) is a new visit and gets its own clip, tagged <step>-<visit> with the reason, so the rework is on camera and the final cut can show the loop",
install.sh+2@@ -23,6 +23,8 @@ for helper in aiflow-widget library-tour; do done ln -sf "$HERE/tools/aiflow-widget.py" "$HOME/.local/bin/adom-aiflow-widget" ln -sf "$HERE/tools/library-tour.py" "$HOME/.local/bin/adom-aiflow-library-tour"+chmod +x "$HERE/tools/kicad-cli-remote"+ln -sf "$HERE/tools/kicad-cli-remote" "$HOME/.local/bin/adom-aiflow-kicad-cli-remote" for skill in "$HERE"/skills/*; do [ -f "$skill/SKILL.md" ] || continue name="$(basename "$skill")"
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.32",+ "version": "0.1.33", "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.32",+ "version": "0.1.33", "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.",
skills/adom-aiflow/SKILL.md+2−2@@ -51,7 +51,7 @@ No recording may outlive an hour (a hard cap on every recording), `finish` and ` ## The spec -`docs/spec-example.json` on the page is the ESC G431's: copper thickness, clearances, the inherited error count, fixed refs (the molecule interface), planes, wide and mid nets, Kelvin pairs, loads per net (amps, max rise), hot parts (watts, tab net), the pours (outline, around parts with a margin and exclusions, or explicit polygons; priorities and connection styles), solid patches, thermal and stitching vias, and the nets that are deliberately not poured. Write the spec from the schematic before you start; it is the electrical judgement, and it is what makes two engines' runs comparable.+`docs/spec-example.json` on the page is the ESC G431's: copper thickness, clearances, the inherited error count, fixed refs (the molecule interface), planes, wide and mid nets, Kelvin pairs, loads per net (amps, max rise), hot parts (watts, tab net), the pours (outline, around parts with a margin and exclusions, or explicit polygons; priorities and connection styles), solid patches, thermal and stitching vias, and the nets that are deliberately not poured. A `thermalVias` entry on a pin too small to hold a via (SOT-23, 0603) gets 0.6/0.3 mm vias in the pour just outside the pad, clear of other nets, the stitch vias and the vias already on the board, never one in the pad (an open via in a small pad wicks the solder); `pour` says how many of `count` fit. Kelvin tap keepouts follow the tap's own layer (both layers only at its vias), stop short of the power pin the tap lands on so that pin keeps its pour, and are numbered per net. Write the spec from the schematic before you start; it is the electrical judgement, and it is what makes two engines' runs comparable. ## The rules behind the stages @@ -155,7 +155,7 @@ Custom manual stages default to AI. `take custom-stage=ai` or `take custom-stage A pour's area or presence is not proof of current capacity or acceptable temperature. `analyze current` and `analyze thermal` now require both the heuristic checks and a matching Adom Fields solve. Run `adom-fields analyze --board <current saved board> --spec <run spec> --out <run>/fields`, then `analyze current --fields <run>/fields/fields.json` and `analyze thermal --fields <run>/fields/fields.json`. Fields records SHA-256 identities for the exact inputs. Changed boards or specs invalidate the result; unsolved loaded nets or hot parts are not a pass. `finish` rechecks the evidence. This is an engineering screen under the solver's documented assumptions, not junction-temperature certification. -Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. Manufacturing-rule warnings, including hole spacing, remain blocking.+Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. In a container without native KiCad, `ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote` (installed with AI Flow) runs `pcb drc` on a desktop's KiCad through Adom Bridge (`KICAD_REMOTE_TARGET`, default ConfRoomROG); a stock distro kicad-cli that lacks `pcb drc --refill-zones` is ignored. The gate refills zones natively when it has a native kicad-cli, so zone fills are KiCad's own, not the raster estimate, and the current and thermal analyses read the board's own copper when the routing plan leaves nets out (an adopted, already-routed board). Manufacturing-rule warnings, including hole spacing, remain blocking. Component library footage: keep the full `library-tour` separate. A reviewed five-second `library-overview` artifact may enter compose, with `--review` binding the exact selected STEP/GLB variants, current board, clip and native readback/screenshot. See [the review contract](https://wiki.adom.inc/adom/adom-aiflow/files/docs/library-overview-review.md). Never claim native model matching from a tour alone.
skills/aiflow-silkscreen/SKILL.md+1−1@@ -36,7 +36,7 @@ Use `adom-aiflow silkscreen-dashboard control --json '{"record":"overview"}' --r A full placement demonstration starts with no authored labels in the candidate occupancy, then attempts every required label. Reuse text content, native glyph metrics and electrical anchors, not earlier accepted positions. Preserve unresolved attempts and subsequent rework in the event history. Front and back are separate collision/placement passes in one run. Filter candidates, selected labels, leaders and playback events by the active face; never overlay the opposite face during a recording. -Show the active label's native text height in millimetres, including distinct reference/value sizes. Maximize readable size near its physical feature; 0.20 mm is a last-resort InstaPCB value, not a default. Prefer short curved leader lines whenever contact/pin association is unclear. Route leaders to the feature's visible rim, keep them off holes, pads, bodies and other text, and verify both faces in the native EDA. Include leaders as actual placement/rework events.+Show the active label's native text height in millimetres, including distinct reference/value sizes. Maximize readable size near its physical feature; 0.20 mm is a last-resort in-house fab value, not a default. Prefer short curved leader lines whenever contact/pin association is unclear. Route leaders to the feature's visible rim, keep them off holes, pads, bodies and other text, and verify both faces in the native EDA. Include leaders as actual placement/rework events. Record each face as a 1920x1080 detailed replay with an explicit replay label. End the review video with the same revision in the user's native EDA, including top and bottom inspection. A dashboard preview or API success is not proof of a faithful native transfer. Keep native raw clips separate from captioned/replayed dashboard clips.
tools/kicad-cli-remoteadded+63@@ -0,0 +1,63 @@+#!/usr/bin/env bash+# kicad-cli-remote: a native KiCad 10 `kicad-cli` for a container that has none, run on a desktop+# through Adom Bridge. Implements what adom-aiflow needs (ADOM_AIFLOW_KICAD_CLI):+# kicad-cli-remote --version+# kicad-cli-remote pcb drc --format json --output <report.json> <board.kicad_pcb>+# The board travels with its sibling .kicad_pro / .kicad_dru so project rules are kept.+# Env: KICAD_REMOTE_TARGET (default ConfRoomROG), KICAD_REMOTE_CLI (native kicad-cli path on it),+# KICAD_REMOTE_THREAD (ai-thread name for the bridge; default kicad-cli-remote).+set -euo pipefail+T="${KICAD_REMOTE_TARGET:-ConfRoomROG}"+TH="${KICAD_REMOTE_THREAD:-kicad-cli-remote}"+# The desktop's native kicad-cli: KICAD_REMOTE_CLI, else what the KiCad bridge reports (install.kicadCli).+CLI="${KICAD_REMOTE_CLI:-}"++if [[ "${1:-}" == "--version" ]]; then echo "kicad-cli-remote -> $T (native KiCad via Adom Bridge)"; exit 0; fi+[[ "${1:-}" == "pcb" && "${2:-}" == "drc" ]] || { echo "kicad-cli-remote: only 'pcb drc' and '--version' are supported" >&2; exit 2; }+shift 2+if [[ " $* " == *" --help "* || " $* " == *" -h "* ]]; then+ echo "Usage: kicad-cli-remote pcb drc [--format json] [--output FILE] [--refill-zones] [--severity-all] INPUT_FILE"+ echo " --refill-zones Refill zones before running DRC (passed to the desktop's native KiCad)"+ exit 0+fi+fmt=json; out=""; board=""; extra=()+while [[ $# -gt 0 ]]; do+ case "$1" in+ --format) fmt="$2"; shift 2;;+ --output|-o) out="$2"; shift 2;;+ --refill-zones|--severity-all|--severity-error|--severity-warning|--all-track-errors) extra+=("$1"); shift;;+ --*) shift;;+ *) board="$1"; shift;;+ esac+done+[[ -f "$board" && -n "$out" ]] || { echo "kicad-cli-remote: need --output and an existing board" >&2; exit 2; }+if [[ -z "$CLI" ]]; then+ CLI=$(adom-bridge --target "$T" kicad_status '{"reason":"find the native kicad-cli for an aiflow DRC"}' --ai-thread "$TH" | python3 -c 'import sys,json;d=json.load(sys.stdin);print((d.get("install") or {}).get("kicadCli",""))')+ [[ -n "$CLI" ]] || { echo "kicad-cli-remote: no KiCad found on $T (set KICAD_REMOTE_CLI)" >&2; exit 2; }+fi+# A temp folder on the desktop: KICAD_REMOTE_TMP, else the desktop's %TEMP%.+RTMP="${KICAD_REMOTE_TMP:-}"+if [[ -z "$RTMP" ]]; then+ RTMP=$(adom-bridge --target "$T" shell_execute '{"command":"echo %TEMP%","reason":"temp folder for an aiflow DRC"}' --ai-thread "$TH" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("stdout","").strip().replace(chr(92),"/"))')+fi+job="aiflow-drc-$(date +%s)-$$"+dest="$RTMP/$job"+base="$(basename "$board" .kicad_pcb)"+files=("$(readlink -f "$board")")+for ext in kicad_pro kicad_dru; do f="${board%.kicad_pcb}.$ext"; [[ -f "$f" ]] && files+=("$(readlink -f "$f")"); done+args=$(python3 -c 'import json,sys;print(json.dumps({"filePaths":sys.argv[2:],"dest":sys.argv[1],"reason":"offline KiCad DRC for adom-aiflow (native kicad-cli on the desktop)"}))' "$dest" "${files[@]}")+adom-bridge --target "$T" send_files "$args" --ai-thread "$TH" >/dev/null+winboard="$dest/$base.kicad_pcb"; winrep="$dest/$base.drc.$fmt"+cmd=$(python3 -c 'import json,sys;cli,b,r,f=sys.argv[1:5];x=" ".join(sys.argv[5:]);c=f"\"{cli}\" pcb drc {x} --format {f} --output \"{r}\" \"{b}\"";print(json.dumps({"command":c.replace("/","\\"),"timeoutSeconds":240,"reason":"offline KiCad DRC for adom-aiflow"}))' "$CLI" "$winboard" "$winrep" "$fmt" "${extra[@]}")+res=$(adom-bridge --target "$T" shell_execute "$cmd" --ai-thread "$TH")+tmp="$(mktemp -d)"+adom-bridge --target "$T" pull_file "$(python3 -c 'import json,sys;print(json.dumps({"filePaths":[sys.argv[1]],"saveTo":sys.argv[2],"reason":"bring the DRC report back"}))' "$winrep" "$tmp")" --ai-thread "$TH" >/dev/null+if [[ -f "$tmp/$base.drc.$fmt" ]]; then+ mv "$tmp/$base.drc.$fmt" "$out"+ adom-bridge --target "$T" shell_execute "$(python3 -c 'import json,sys;print(json.dumps({"command":"rmdir /s /q \""+sys.argv[1].replace("/","\\")+"\"","reason":"clean up the DRC temp folder"}))' "$dest")" --ai-thread "$TH" >/dev/null || true+ rmdir "$tmp" 2>/dev/null || true+ exit 0+fi+echo "kicad-cli-remote: no report came back" >&2+echo "$res" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("stdout","")[-800:],d.get("stderr","")[-800:])' >&2 || true+exit 1
tools/silkscreen-dashboard/SKILL.md+1−1@@ -30,7 +30,7 @@ Use `adom-aiflow silkscreen-dashboard control --json '{"record":"overview"}' --r A full placement demonstration starts with no authored labels in the candidate occupancy, then attempts every required label. Reuse text content, native glyph metrics and electrical anchors, not earlier accepted positions. Preserve unresolved attempts and subsequent rework in the event history. Front and back are separate collision/placement passes in one run. Filter candidates, selected labels, leaders and playback events by the active face; never overlay the opposite face during a recording. -Show the active label's native text height in millimetres, including distinct reference/value sizes. Maximize readable size near its physical feature; 0.20 mm is a last-resort InstaPCB value, not a default. Prefer short curved leader lines whenever contact/pin association is unclear. Route leaders to the feature's visible rim, keep them off holes, pads, bodies and other text, and verify both faces in the native EDA. Include leaders as actual placement/rework events.+Show the active label's native text height in millimetres, including distinct reference/value sizes. Maximize readable size near its physical feature; 0.20 mm is a last-resort in-house fab value, not a default. Prefer short curved leader lines whenever contact/pin association is unclear. Route leaders to the feature's visible rim, keep them off holes, pads, bodies and other text, and verify both faces in the native EDA. Include leaders as actual placement/rework events. Record each face as a 1920x1080 detailed replay with an explicit replay label. End the review video with the same revision in the user's native EDA, including top and bottom inspection. A dashboard preview or API success is not proof of a faithful native transfer. Keep native raw clips separate from captioned/replayed dashboard clips.
uninstall.sh+1−1@@ -1,4 +1,4 @@ #!/bin/bash-rm -f "$HOME/.local/bin/adom-aiflow"+rm -f "$HOME/.local/bin/adom-aiflow" "$HOME/.local/bin/adom-aiflow-kicad-cli-remote" rm -rf "$HOME/.claude/skills/adom-aiflow" "$HOME/.claude/skills/aiflow-measurement" echo "OK: adom-aiflow removed."