← Commit history

Support explicit KiCad read endpoints without redirecting mutations; preserve native gates

John Lauer ·6ffc663d2b · ·parent 4160ace
3 files changed +60
SKILL.md+4
@@ -433,3 +433,7 @@ The server uses one native filesystem watcher per run and a shared state cache. ## Project wiki presentation before delivery  Read `skills/aiflow-project-page/SKILL.md` and run `adom-aiflow project-page-check --project <root>` before delivering a wiki-backed project. Explicitly bind the assembled board hero and project datasheet, include 5–8 reviewed README screenshots, place the datasheet below the README and verify the actual published page. Automatic asset discovery and upload success are not acceptance.++### Explicit native read endpoint++For multiple-editor IPC conflicts, read `docs/isolated-kicad-read-socket.md`. `ADOM_AIFLOW_KICAD_READ_SOCKET` selects a verified instance for the four read/validation verbs only; it does not redirect mutations or window capture. Preserve unsaved editors, require live-editor identity, and keep every native DRC gate. A renamed PCB checkpoint must carry matching project/rule sidecars before it is opened.
crates/aiflow-bridge/src/lib.rs+45
@@ -94,6 +94,21 @@ fn pull_until_landed(local: &Path, attempts: usize, mut pull: impl FnMut(), mut     (false, attempts.max(1)) } +/// Opt-in endpoint for the four native read/validate verbs whose published+/// schemas support socketPath. Never silently redirect a mutation or a window verb.+fn read_socket_args(verb: &str, args: &Value, socket: Option<&str>) -> Value {+    let mut result = args.clone();+    if matches!(verb, "kicad_routing_state" | "kicad_routing_validate" |+        "kicad_placement_state" | "kicad_placement_validate") {+        if let (Some(object), Some(socket)) = (result.as_object_mut(), socket) {+            if !socket.trim().is_empty() && !object.contains_key("socketPath") {+                object.insert("socketPath".into(), Value::String(socket.into()));+            }+        }+    }+    result+}+ #[derive(Clone)] pub struct Bridge {     pub ai_thread: String,@@ -113,6 +128,8 @@ impl Bridge {      /// One adom-bridge verb, as is.     pub fn raw_call(&self, verb: &str, args: &Value) -> Value {+        let socket = std::env::var("ADOM_AIFLOW_KICAD_READ_SOCKET").ok();+        let args = read_socket_args(verb, args, socket.as_deref());         let out = Command::new("adom-bridge").args(["--ai-thread", &self.ai_thread, "--target", &self.target, verb, &args.to_string()]).output();         match out {             Ok(o) => {@@ -521,3 +538,31 @@ mod pull_parts_tests {         assert_eq!(super::base64_encode(b"Ma"), "TWE=");     } }++#[cfg(test)]+mod read_socket_tests {+    use super::*;+    #[test]+    fn scoped_reads_preserve_identity_and_explicit_socket() {+        let a=json!({"filePath":"exact-board","expectedRevision":"r1"});+        for verb in ["kicad_routing_state","kicad_routing_validate","kicad_placement_state","kicad_placement_validate"] {+            let b=read_socket_args(verb,&a,Some("ipc://isolated/api.sock"));+            assert_eq!(b["socketPath"],"ipc://isolated/api.sock");+            assert_eq!(b["filePath"],a["filePath"]);+            assert_eq!(b["expectedRevision"],a["expectedRevision"]);+        }+        let explicit=json!({"socketPath":"ipc://explicit/api.sock"});+        assert_eq!(read_socket_args("kicad_placement_state",&explicit,Some("other")),explicit);+    }+    #[test]+    fn absent_empty_and_unrelated_calls_are_unchanged() {+        let a=json!({"filePath":"board"});+        for socket in [None,Some(""),Some("  ")] {+            assert_eq!(read_socket_args("kicad_placement_state",&a,socket),a);+        }+        for verb in ["kicad_move_footprint","kicad_open_board","kicad_apply_tracks","fusion_routing_state","desktop_record_window_start"] {+            assert_eq!(read_socket_args(verb,&a,Some("ipc://isolated/api.sock")),a);+        }+        assert_eq!(read_socket_args("kicad_placement_state",&Value::Null,Some("x")),Value::Null);+    }+}
docs/isolated-kicad-read-socket.mdadded+11
@@ -0,0 +1,11 @@+# Read-only checks with multiple KiCad editors++Some Windows KiCad 10 sessions expose only one default IPC pipe even with multiple PCB editors. A saved-file fallback is not live placement evidence. Keep a board mismatch failed; never close unrelated unsaved editors or relabel file snapshots as live.++For an independently verified native instance, `ADOM_AIFLOW_KICAD_READ_SOCKET=ipc://<exact-pipe>` selects its endpoint for only `kicad_placement_state`, `kicad_placement_validate`, `kicad_routing_state`, and `kicad_routing_validate`. An explicit `socketPath` in the call wins. No mutation, window-control, capture, Fusion or other verb is redirected. This is not general multiple-instance mutation support.++Verify the returned board path, revision, `source: live-editor`, native validation and window identity. Existing gates still compare saved and live footprints, exact revision, complete DRC and reviewed edge-connector geometry. An invalid endpoint or file fallback must fail those gates.++A task-owned instance can be launched with a process-local TEMP/TMP directory to give its API pipe a separate name. This workaround requires native verification on the selected desktop; it is not a promise for every KiCad build. Do not change global environment/configuration or replace a shared runtime. Native edits needing endpoint selection remain a bridge-owned capability gap until their schemas explicitly support it.++When preparing renamed PCB checkpoints, copy matching `.kicad_pro` and `.kicad_dru` sidecars to the output basename before opening it. Copying only the input basename can cause KiCad to create a default project for the output file, changing the DRC rules. Close only the saved task-owned editor before replacing its sidecars, then reopen and verify the actual rules. Never adjust acceptance thresholds to match an accidental default project.