← Commit history
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.85"+version = "0.1.86" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.85"+version = "0.1.86" dependencies = [  "aiflow-board",  "serde",@@ -34,7 +34,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.85"+version = "0.1.86" dependencies = [  "roxmltree",  "serde",@@ -43,7 +43,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.85"+version = "0.1.86" dependencies = [  "aiflow-board",  "serde",@@ -52,7 +52,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.85"+version = "0.1.86" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -62,7 +62,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.85"+version = "0.1.86" dependencies = [  "aiflow-board",  "serde",@@ -71,7 +71,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.85"+version = "0.1.86" dependencies = [  "aiflow-board",  "serde",@@ -80,7 +80,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.85"+version = "0.1.86" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -90,7 +90,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.85"+version = "0.1.86" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -100,7 +100,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.85"+version = "0.1.86" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.85"+version = "0.1.86" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+4
@@ -393,3 +393,7 @@ Derive continuous heat cases from the actual voltage/current/power envelope and ### Repeated physical pads in explicit current boundaries  When multiple physical lands share one logical pin number, use the reviewed Fields physical-pad selector (`at: [x_mm, y_mm]`, adom/adom-fields PR10 and dependencies). Declare each branch current and its assumption; never silently divide the load to obtain a pass. AIFlow resolves and injects current at each declared physical pad, then matches each reported boundary once, including its coordinates. Missing, ambiguous, duplicate or changed selectors refuse; integer and decimal coordinate spellings are equivalent. Recheck coordinates after moving a footprint. Preserve owner review and shared runtimes; source support is not a deployed release or package-resistance model.++### Existing pours after board adoption++Use `verify-pours --plan <reviewed-plan.json>` for read-only validation of already present saved pours against a reconciled explicit plan. Read `docs/existing-pours.md`. It records input-bound verification, not creation ownership; never replay a native mutation simply to repair dashboard evidence. A stale plan must fail. Native DRC, measurement and electrical/thermal gates remain required.
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/continuation.rs+7
@@ -128,6 +128,13 @@ pub fn assess(data: &Value, pending: &[String], stop: Option<Result<Value, Strin             ));         }     }+    if data["outcomes"]["land-pours"]["mode"]=="verified-existing" {+        let board=data["boards"].as_array().and_then(|a|a.last()).and_then(Value::as_str).unwrap_or("");+        let spec=data["spec"].as_str().unwrap_or("");+        if !crate::pourverify::bound(&data["outcomes"]["land-pours"],std::path::Path::new(board),std::path::Path::new(spec)) {+            remaining.push("verify-pours: board, spec or plan binding changed; verify again".into());+        }+    }     let complete = remaining.is_empty();     let mut decision = if complete { "complete" } else { "continue" };     let mut reason = if complete {
crates/adom-aiflow/src/main.rs+27−4
@@ -1,6 +1,7 @@ //! adom-aiflow: the board loop as one command, with the clock, the negotiation and the finish line. //! Every reply starts with OK: or ERROR: and carries Hint: lines that depend on this board and run. mod continuation;+mod pourverify; mod dashboard; mod silkscreen_sections; mod sequence;@@ -132,6 +133,8 @@ enum Cmd {     /// Replay live through the bridge: route, vias, pours, moves     Land { what: String, #[arg(long)] plan: Option<String>, #[arg(long)] moves: Option<String>, #[arg(long, default_value_t = 600)] pause_ms: u64, /// route/vias: start at this trace number (1-based, as the progress lines count); traces already on the live board are skipped anyway     #[arg(long)] from: Option<usize> },+    /// Verify existing saved filled zones against an explicit reviewed plan, without landing or claiming ownership.+    VerifyPours { #[arg(long)] plan: String },     /// Filled copper per layer from the live board     Measure,     /// current or thermal@@ -1621,7 +1624,7 @@ fn build_report(r: &Run, dir: &Path, page: &str) -> (PathBuf, String, PathBuf) {     if let Some(x) = o.get("route") { md.push_str(&format!("- routing: {} unrouted after {} passes\n", x["unrouted"], x["passes"])); }     if let Some(x) = o.get("gate") { md.push_str(&format!("- gate: {} new DRC errors, {} unconnected, {} inherited\n", x["newErrors"], x["unconnected"], x["inheritedErrors"])); }     if let Some(x) = o.get("land-route") { md.push_str(&format!("- landed live: {} of {} route entries\n", x["accepted"], x["entries"])); }-    if let Some(x) = o.get("land-pours") { md.push_str(&format!("- pours: {} groups, failed {}\n", x["groups"].as_array().map(|a| a.len()).unwrap_or(0), x["failed"])); }+    if let Some(x) = o.get("land-pours") { md.push_str(&format!("- pours ({}): {} groups, failed {}\n", x["mode"].as_str().unwrap_or("landed"), x["groups"].as_array().map(|a| a.len()).unwrap_or(0), x["failed"])); }     if let Some(x) = o.get("measure") { md.push_str(&format!("- copper kept (mm2 per layer): {}\n", x["filledAreaByLayerMm2"])); }     for k in ["analyze-current", "analyze-thermal"] { if let Some(x) = o.get(k) { md.push_str(&format!("- {k}: {}\n", if x["pass"].as_bool() == Some(true) { "pass" } else { "fail" })); } }     md.push_str("\n");@@ -2931,6 +2934,26 @@ fn main() {                 other => err(&format!("land {other}: use moves, route, vias or pours"), &[]),             }         }+        Cmd::VerifyPours { plan } => {+            let mut r = load_run(&cli);+            let board_hash = pourverify::hash(Path::new(&r.current_board())).unwrap_or_else(|e|err(&e,&[]));+            let plan_hash = pourverify::hash(Path::new(plan)).unwrap_or_else(|e|err(&e,&[]));+            let b = board_of(&r);+            let mut proof = pourverify::check(&b, &read_json(plan)).unwrap_or_else(|e| err(&e, &[]));+            if pourverify::hash(Path::new(&r.current_board())).ok().as_deref()!=Some(&board_hash)+                || pourverify::hash(Path::new(plan)).ok().as_deref()!=Some(&plan_hash) {+                err("board or plan changed during verification; retry after the writer finishes", &[]);+            }+            proof["boardSha256"] = json!(board_hash);+            proof["specSha256"] = json!(pourverify::hash(Path::new(r.data["spec"].as_str().unwrap_or(""))).unwrap_or_else(|e|err(&e,&[])));+            proof["planSha256"] = json!(plan_hash);+            proof["plan"] = json!(std::fs::canonicalize(plan).unwrap_or_else(|e|err(&e.to_string(),&[])));+            write_json(&dir.join("verify-pours.json"), &proof);+            r.outcome("land-pours", proof.clone());+            r.save().unwrap();+            if proof["pass"] != true { err(&format!("saved pours do not match the plan: {}",proof["failed"]), &["Review the actual geometry and intended plan; never replay pours merely to restore evidence. No board or ownership was changed.".into()]); }+            ok("existing saved pours match the reviewed plan; no copper or ownership changed", &["Native DRC, live measurement, current and thermal checks remain required. Changed board, spec or plan invalidates this proof.".into()]);+        }         Cmd::Measure => {             let mut r = load_run(&cli);             let br = bridge_of(&r, &cli);@@ -3015,8 +3038,8 @@ fn main() {             if !o.contains_key("land-route") {                 missing.push("land route".into());             }-            if o.get("land-pours").map(|p| !p["failed"].as_array().map(|a| a.is_empty()).unwrap_or(false)).unwrap_or(true) {-                missing.push("land pours (every planned pour accepted)".into());+            if o.get("land-pours").map(|p| !p["failed"].as_array().map(|a| a.is_empty()).unwrap_or(false) || !pourverify::bound(p, Path::new(&r.current_board()), Path::new(r.data["spec"].as_str().unwrap_or("")))).unwrap_or(true) {+                missing.push("land pours or verify-pours (every planned zone accepted; current inputs required)".into());             }             if !measurement_usable(o.get("measure")) {                 missing.push("measure (successful native copper readback)".into());@@ -3220,7 +3243,7 @@ fn main() {             ok(&format!("run {}, engine {}, {:.1} min since the prompt ({})\nstages (min): {}\ndecisions: {}\nroute: {} unrouted; gate: {} new / {} unconnected; pours: {}; measure: {}; current: {}; thermal: {}; finished: {}",                 dir.display(), r.data["engine"], r.elapsed_minutes(), r.data["clock"]["promptTime"], Value::Object(r.stage_minutes()), r.data["decisions"],                 o.get("route").map(|x| x["unrouted"].to_string()).unwrap_or("-".into()), o.get("gate").map(|x| x["newErrors"].to_string()).unwrap_or("-".into()), o.get("gate").map(|x| x["unconnected"].to_string()).unwrap_or("-".into()),-                if o.get("land-pours").map(|p| p["failed"].as_array().map(|a| a.is_empty()).unwrap_or(false)).unwrap_or(false) { "landed" } else { "no" }, o.get("measure").map(|x| x["filledAreaByLayerMm2"].to_string()).unwrap_or("-".into()),+                if o.get("land-pours").map(|p| p["failed"].as_array().map(|a| a.is_empty()).unwrap_or(false)).unwrap_or(false) { if o["land-pours"]["mode"]=="verified-existing" { "verified existing" } else { "landed" } } else { "no" }, o.get("measure").map(|x| x["filledAreaByLayerMm2"].to_string()).unwrap_or("-".into()),                 o.get("analyze-current").map(|x| x["pass"].to_string()).unwrap_or("-".into()), o.get("analyze-thermal").map(|x| x["pass"].to_string()).unwrap_or("-".into()), r.data["clock"]["finishTime"]), &[step_table(&r)]);         }         Cmd::Report { page, push, refresh, user_machine, surface } => {
crates/adom-aiflow/src/pourverify.rsadded+358
@@ -0,0 +1,358 @@+//! Read-only comparison of a saved board with an explicit pour plan.+//! This creates no zone ownership and never changes native copper.+use aiflow_board::{sx::Node, Board};+use serde_json::{json, Value};+use sha2::{Digest, Sha256};+use std::path::Path;++pub fn hash(path: &Path) -> Result<String, String> {+    std::fs::read(path)+        .map(|b| format!("{:x}", Sha256::digest(b)))+        .map_err(|e| e.to_string())+}+fn number(n: &Node, key: &str, default: f64) -> f64 {+    n.find(key).and_then(|v| v.number(1)).unwrap_or(default)+}+fn canonical(n: &Node) -> Value {+    let mut children: Vec<Value> = n.nodes().map(canonical).collect();+    if n.tag != "pts" {+        children.sort_by_key(Value::to_string);+    }+    let atoms: Vec<Value> = n+        .items+        .iter()+        .filter_map(|i| match i {+            aiflow_board::sx::Item::Atom { text, .. } => Some(+                text.parse::<f64>()+                    .ok()+                    .filter(|x| x.is_finite())+                    .map(|x| json!(x))+                    .unwrap_or_else(|| json!(text)),+            ),+            _ => None,+        })+        .collect();+    json!([n.tag, atoms, children])+}+fn signature(board: &Board, n: &Node) -> Value {+    let mut layers: Vec<String> = n+        .find("layer")+        .or_else(|| n.find("layers"))+        .map(|x| (1..).map_while(|i| x.atom(i)).map(str::to_string).collect())+        .unwrap_or_default();+    layers.sort();+    let keepout = n.find("keepout");+    let mut v = json!({"layers":layers, "polygons":n.find_all("polygon").map(canonical).collect::<Vec<_>>()});+    if let Some(k) = keepout {+        v["keepout"] = canonical(k);+        // Placement rule areas are independent of copper keepouts; refuse active ones.+        v["placement"] = json!(n+            .find("placement")+            .and_then(|x| x.find("enabled"))+            .and_then(Node::value)+            .unwrap_or("no"));+    } else {+        v["net"] = json!(board.item_net(n));+        v["priority"] = json!(number(n, "priority", 0.0));+        v["minThickness"] = json!(number(n, "min_thickness", 0.25));+        let cp = n.find("connect_pads");+        v["connection"] = json!(cp.and_then(Node::value).unwrap_or("thermal"));+        v["clearance"] = json!(cp.map(|x| number(x, "clearance", 0.0)).unwrap_or(0.0));+        let fill = n.find("fill");+        for (key, default) in [+            ("thermal_gap", 0.5),+            ("thermal_bridge_width", 0.5),+            ("island_removal_mode", 0.0),+            ("island_area_min", 10.0),+            ("mode", 0.0),+        ] {+            v[key] = json!(fill.map(|x| number(x, key, default)).unwrap_or(default));+        }+    }+    v+}++pub fn check(board: &Board, plan: &Value) -> Result<Value, String> {+    let zones = plan["zones"]+        .as_array()+        .filter(|a| !a.is_empty())+        .ok_or("nonempty zones plan required")?;+    let resolved = aiflow_pours::resolve_outline(board, zones);+    let actual: Vec<_> = board.root.find_all("zone").collect();+    let mut used = std::collections::BTreeSet::new();+    let mut groups = Vec::new();+    let mut failed = Vec::new();+    for (index, z) in resolved.iter().enumerate() {+        let label = z["name"].as_str().unwrap_or("unnamed zone");+        let allowed = [+            "name",+            "net",+            "layers",+            "polygon",+            "priority",+            "connection",+            "clearance",+            "minThickness",+            "thermalGap",+            "thermalBridge",+            "type",+            "keepout",+        ];+        if let Some(key) = z+            .as_object()+            .ok_or("zone must be an object")?+            .keys()+            .find(|k| !allowed.contains(&k.as_str()))+        {+            return Err(format!("unsupported plan property {key} in {label}"));+        }+        if z["layers"].as_array().is_none_or(|a| {+            a.is_empty()+                || a.iter().any(|l| {+                    !l.as_str()+                        .is_some_and(|l| board.copper_layers.iter().any(|b| b == l))+                })+        }) {+            return Err(format!("{label}: explicit board copper layers required"));+        }+        if z["polygon"].as_array().is_none_or(|a| {+            a.len() < 3+                || a.iter().any(|p| {+                    p.as_array().is_none_or(|p| {+                        p.len() != 2 || p.iter().any(|x| !x.as_f64().is_some_and(f64::is_finite))+                    })+                })+        }) {+            return Err(format!("{label}: finite polygon coordinates required"));+        }+        for key in ["clearance", "minThickness", "thermalGap", "thermalBridge"] {+            if !z[key].is_null() && !z[key].as_f64().is_some_and(|x| x.is_finite() && x >= 0.0) {+                return Err(format!("{label}: invalid {key}"));+            }+        }+        if !z["priority"].is_null() && z["priority"].as_u64().is_none() {+            return Err(format!("{label}: invalid priority"));+        }+        if !z["connection"].is_null()+            && !matches!(z["connection"].as_str(), Some("solid" | "full" | "thermal"))+        {+            return Err(format!("{label}: unsupported connection"));+        }+        if !z["type"].is_null() && !matches!(z["type"].as_str(), Some("keepout" | "copper")) {+            return Err(format!("{label}: unsupported zone type"));+        }+        if let Some(k) = z.get("keepout") {+            let k = k.as_object().ok_or("keepout must be an object")?;+            if k.iter().any(|(key, v)| {+                !["tracks", "vias", "pads", "copperPour", "footprints"].contains(&key.as_str())+                    || !v.is_boolean()+            }) {+                return Err(format!("{label}: unsupported keepout setting"));+            }+        }+        let expected = aiflow_board::sx::parse(&aiflow_pours::zone_sexpr(board, z)?)?;+        let wanted = signature(board, &expected);+        let matches: Vec<_> = actual+            .iter()+            .enumerate()+            .filter(|(i, n)| !used.contains(i) && signature(board, n) == wanted)+            .collect();+        if matches.len() != 1 {+            failed.push(format!(+                "{index}: {label}: expected one exact geometry/settings match, found {}",+                matches.len()+            ));+            continue;+        }+        let (i, n) = matches[0];+        used.insert(i);+        let supported = [+            "net",+            "net_name",+            "layer",+            "layers",+            "uuid",+            "name",+            "hatch",+            "priority",+            "connect_pads",+            "min_thickness",+            "filled_areas_thickness",+            "fill",+            "polygon",+            "filled_polygon",+            "keepout",+            "placement",+            "locked",+        ];+        for child in n.nodes().filter(|x| !supported.contains(&x.tag.as_str())) {+            failed.push(format!(+                "{label}: unsupported native zone field {}",+                child.tag+            ));+        }+        if let Some(fill) = n.find("fill") {+            for child in fill.nodes().filter(|x| {+                ![+                    "thermal_gap",+                    "thermal_bridge_width",+                    "island_removal_mode",+                    "island_area_min",+                    "mode",+                ]+                .contains(&x.tag.as_str())+            }) {+                failed.push(format!(+                    "{label}: unsupported native fill field {}",+                    child.tag+                ));+            }+        }+        if n.find("keepout").is_none() {+            let layers = wanted["layers"].as_array().unwrap();+            for layer in layers {+                let filled = n.find_all("filled_polygon").any(|p| {+                    p.find("layer").and_then(Node::value) == layer.as_str()+                        && p.find("pts")+                            .is_some_and(|pts| pts.find_all("xy").count() >= 3)+                });+                if !filled {+                    failed.push(format!("{label}: no saved filled polygon on {layer}"));+                }+            }+        }+        groups.push(json!({"name":label,"boardZoneIndex":i,"ok":true}));+    }+    // No hidden extra copper or keepout: every saved zone needs an explicit plan entry.+    for (i, n) in actual.iter().enumerate().filter(|(i, _)| !used.contains(i)) {+        failed.push(format!(+            "unplanned board zone {i}: {}",+            n.find("name").and_then(Node::value).unwrap_or("unnamed")+        ));+    }+    Ok(+        json!({"mode":"verified-existing","pass":failed.is_empty(),"failed":failed,"groups":groups,+        "ownershipGranted":false,"scope":"saved filled polygons and planned zone geometry/settings; native DRC, measured copper and electrical/thermal checks remain separate"}),+    )+}++pub fn bound(outcome: &Value, board: &Path, spec: &Path) -> bool {+    if outcome["mode"] != "verified-existing" {+        return true;+    }+    outcome["boardSha256"].as_str() == hash(board).ok().as_deref()+        && outcome["specSha256"].as_str() == hash(spec).ok().as_deref()+        && outcome["plan"]+            .as_str()+            .is_some_and(|p| outcome["planSha256"].as_str() == hash(Path::new(p)).ok().as_deref())+        && outcome["boardSha256"].is_string()+        && outcome["specSha256"].is_string()+        && outcome["planSha256"].is_string()+}++#[cfg(test)]+mod tests {+    use super::*;+    fn fixture() -> (Board, Value) {+        let p = json!({"zones":[{"net":"GND","layers":["F.Cu"],"polygon":[[0,0],[5,0],[5,5]],"connection":"solid"}]});+        let prefix = r#"(kicad_pcb (version 20260101) (layers (0 "F.Cu" signal) (31 "B.Cu" signal)) (gr_rect (start 0 0) (end 10 10) (layer "Edge.Cuts"))"#;+        let b = Board::from_text("test", format!("{prefix})")).unwrap();+        let z = aiflow_pours::zone_sexpr(&b, &p["zones"][0]).unwrap();+        let z = z.trim();+        let z = format!(+            r#"{} (filled_polygon (layer "F.Cu") (pts (xy 0 0) (xy 5 0) (xy 5 5))))"#,+            &z[..z.len() - 1]+        );+        let b = Board::from_text("test", format!("{prefix} {z})")).unwrap();+        (b, p)+    }+    #[test]+    fn existing_filled_plan_passes_without_ownership() {+        let (b, p) = fixture();+        let r = check(&b, &p).unwrap();+        assert_eq!(r["pass"], true);+        assert_eq!(r["ownershipGranted"], false);+    }+    #[test]+    fn changed_geometry_refuses() {+        let (b, mut p) = fixture();+        p["zones"][0]["polygon"][0][0] = json!(1);+        assert_eq!(check(&b, &p).unwrap()["pass"], false);+    }+    #[test]+    fn changed_net_refuses() {+        let (b, mut p) = fixture();+        p["zones"][0]["net"] = json!("POWER");+        assert_eq!(check(&b, &p).unwrap()["pass"], false);+    }+    #[test]+    fn empty_plan_refuses() {+        let (b, _) = fixture();+        assert!(check(&b, &json!({"zones":[]})).is_err());+    }+    #[test]+    fn duplicate_plan_refuses() {+        let (b, mut p) = fixture();+        let z = p["zones"][0].clone();+        p["zones"].as_array_mut().unwrap().push(z);+        assert_eq!(check(&b, &p).unwrap()["pass"], false);+    }+    #[test]+    fn changed_clearance_refuses() {+        let (b, mut p) = fixture();+        p["zones"][0]["clearance"] = json!(0.3);+        assert_eq!(check(&b, &p).unwrap()["pass"], false);+    }+    #[test]+    fn changed_layer_refuses() {+        let (b, mut p) = fixture();+        p["zones"][0]["layers"] = json!(["B.Cu"]);+        assert_eq!(check(&b, &p).unwrap()["pass"], false);+    }+    #[test]+    fn missing_fill_refuses() {+        let (b, p) = fixture();+        let text = b.text.replace("filled_polygon", "removed_polygon");+        let b = Board::from_text("test", text).unwrap();+        assert_eq!(check(&b, &p).unwrap()["pass"], false);+    }+    #[test]+    fn invalid_connection_refuses() {+        let (b, mut p) = fixture();+        p["zones"][0]["connection"] = json!("none");+        assert!(check(&b, &p).is_err());+    }+    #[test]+    fn missing_binding_refuses() {+        assert!(!bound(+            &json!({"mode":"verified-existing"}),+            Path::new("/no/board"),+            Path::new("/no/spec")+        ));+    }+    #[test]+    fn changed_file_invalidates_binding() {+        let dir = std::env::temp_dir().join(format!("aiflow-pour-binding-{}", std::process::id()));+        std::fs::create_dir_all(&dir).unwrap();+        let b = dir.join("board");+        let p = dir.join("plan");+        let s = dir.join("spec");+        for f in [&b, &p, &s] {+            std::fs::write(f, "original").unwrap();+        }+        let v = json!({"mode":"verified-existing","boardSha256":hash(&b).unwrap(),"specSha256":hash(&s).unwrap(),"plan":p,"planSha256":hash(&p).unwrap()});+        assert!(bound(&v, &b, &s));+        for f in [&b, &p, &s] {+            std::fs::write(f, "changed").unwrap();+            assert!(!bound(&v, &b, &s));+            std::fs::write(f, "original").unwrap();+        }+        std::fs::remove_dir_all(dir).unwrap();+    }+    #[test]+    fn unknown_setting_refuses() {+        let (b, mut p) = fixture();+        p["zones"][0]["typo"] = json!(1);+        assert!(check(&b, &p).is_err());+    }+}
crates/aiflow-run/src/lib.rs+1−1
@@ -277,7 +277,7 @@ impl Run {             ("route", _) | ("gate", _) => "routing",             ("land", "moves") => "placement",             ("land", "route") | ("land", "vias") => "routing",-            ("pour", _) | ("land", "pours") | ("measure", _) => "pours",+            ("verify-pours", _) | ("pour", _) | ("land", "pours") | ("measure", _) => "pours",             ("analyze", "current") => "current",             ("analyze", "thermal") => "thermal",             ("capture", _) => "capture",
docs/existing-pours.mdadded+9
@@ -0,0 +1,9 @@+# Verify existing pours without landing them again++Adopting a board invalidates prior landing evidence. If reviewed manual edits changed the copper, first reconcile an explicit zone plan with the intended design. Use `adom-aiflow --run <run> verify-pours --plan <reviewed-plan.json>` to compare that plan with the current saved board. This does not alter the PCB, refill zones, create an undo step, or grant zone ownership.++The verifier requires a nonempty plan, one-to-one matching of every board zone, polygon ordering, copper net/layers, connection/clearance/priority/thickness, thermal and island settings, keepout flags and saved fill polygons per copper layer. It refuses unknown plan properties and unsupported native fill settings. It is deliberately conservative: equivalent reordered polygon vertices can fail. Review differences; do not blindly regenerate a plan from the board to conceal unintended edits.++The existing landing-evidence slot records `mode: verified-existing`, without item IDs or ownership changes. Finish checks SHA-256 bindings to the board, spec and plan. Native DRC, live copper measurement, current and thermal acceptance remain separate requirements. Saved fill presence alone is not proof of native fill freshness, current capacity or thermal performance.++Do not replay `land pours` merely to restore dashboard evidence. That command is a mutation with native ownership rules. Preserve successful creation receipts and never infer ownership from names or geometry.
docs/release-0.1.86.mdadded+5
@@ -0,0 +1,5 @@+# 0.1.86: read-only verification of existing pours++`verify-pours --plan <reviewed-plan.json>` validates the current saved filled zones against an explicit plan after adoption or manual edits. It records `verified-existing` evidence without changing copper or granting ownership. Board, spec and plan hashes bind the result; finish and the continuation guard reject stale bindings. Native DRC, measurement and electrical/thermal checks remain required.++The electronic-load v85 regression rejects the old D1 polygons and accepts the reconciled construction plan. No board bytes change. See [existing pour verification](existing-pours.md) for scope and conservative matching limits.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.85",+  "version": "0.1.86",   "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.85",+  "version": "0.1.86",   "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.",