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.
place verify refuses nested copper-only footprints on box overlaps that KiCad's exact DRC passes
What happens
adom-aiflow place verify (0.2.6, unchanged in 0.2.7) refuses a correct placement with "native placement has unresolved courtyardOverlaps" (and "withoutCourtyard") when a board has nested copper-only footprints, even though KiCad's exact placement DRC on the same live board is clean.
The board was a rotary encoder sensor: generated electrode rings, a guard ring and mounting holes are footprints (listed in spec.fixedRefs) that sit inside one another. The bridge's courtyardOverlaps judges courtyards as bounding boxes, and a footprint without a courtyard by its pad extent, so concentric rings always overlap as boxes. The verdict then also fails its own consistency check (placed vs. outsideOutline).
Cause
crates/adom-aiflow/src/placement_verify.rs, around line 88:
if validation["placed"] != Value::Bool(outside.is_empty()) {return Err("inconsistent native placement verdict".into())}
for key in ["parked","courtyardOverlaps","withoutCourtyard"] {
if !validation[key].as_array().is_some_and(|a|a.is_empty()) { return Err(format!("native placement has unresolved {key}")); }
}
Any box overlap refuses, with no way to say "these are generated, locked copper and KiCad's exact check is clean".
Fix we used
Accept box overlaps only when both parts are in spec.fixedRefs and KiCad's own DRC (already required by the same function) reports no courtyard problem; accept a missing courtyard only for fixedRefs; refuse everything else, and name the offending pairs in the error:
let fixed:BTreeSet<&str>=spec["fixedRefs"].as_array().map(|a|a.iter().filter_map(Value::as_str).collect()).unwrap_or_default();
let overlaps=validation["courtyardOverlaps"].as_array().ok_or("missing courtyardOverlaps")?;
let open_overlaps:Vec<&Value>=overlaps.iter().filter(|o|!(fixed.contains(o["a"].as_str().unwrap_or("")) && fixed.contains(o["b"].as_str().unwrap_or("")))).collect();
let no_cy=validation["withoutCourtyard"].as_array().ok_or("missing withoutCourtyard")?;
let open_no_cy:Vec<&Value>=no_cy.iter().filter(|r|!fixed.contains(r.as_str().unwrap_or(""))).collect();
let placed_box = outside.is_empty() && overlaps.is_empty();
if validation["placed"] != Value::Bool(placed_box) {return Err("inconsistent native placement verdict".into())}
if !validation["parked"].as_array().is_some_and(|a|a.is_empty()) { return Err("native placement has unresolved parked".into()); }
if !open_overlaps.is_empty() { return Err(format!("native placement has unresolved courtyardOverlaps: {}", serde_json::to_string(&open_overlaps).unwrap_or_default())); }
if !open_no_cy.is_empty() { return Err(format!("native placement has unresolved withoutCourtyard: {}", serde_json::to_string(&open_no_cy).unwrap_or_default())); }
A cleaner long-term fix may be for the KiCad bridge to report exact-shape courtyard overlaps; that is the bridge's call. Either way, the error should name the pairs: "unresolved courtyardOverlaps" alone sent us digging.
Patch
aiflow/tool-local/aiflow-0.2.6-capencoder.patch on john/capacitive-encoder (private; the hunk above is the whole fix): https://wiki.adom.inc/john/capacitive-encoder
Found by a Claude Opus 5.5 thread running the KiCad lane on 2026-10-20; with the fix, 41 placements verified live.