← Commit history

rust: retired plugin verbs answer as status; phase 4 etiquette and phase 5 tour-input hooks on the Platform trait; demo placeholder; plan: phase 3 gate 72/3/15

John Lauer ·59eb37ac55 ·27d ago ·parent 012b038
8 files changed +62−2
docs/rust-port-plan.md+1
@@ -15,6 +15,7 @@ Written 2026-09-11 from a full audit of the 0.9.350 source, the Adom Bridge (ab) | 2026-09-11 | Phase 2 landed in code: the nine routing verbs on `kicad-ipc-rs` (revision is the SHA-256 of the board as KiCad serialises it, one native commit per call, DRC on a disposable snapshot with `--refill-zones`), the four file-based routing verbs on a Rust port of the PCB parser (the routed file is byte-identical to the Python's apart from UUIDs), and `kicad_autoroute` with the ai engine returning a plan request and freerouting reserved for phase 2b. 139 tests; the six-net fixture routed on disk passes service-kicad DRC with zero errors. 59 verbs in the binary. Live IPC verification on ConfRoomROG is pending: the box dropped off the relay right after the phase 2 binary was staged (winvm went with it, which points at the network rather than the binary). Phase 3 started: the Windows and UI Automation crates cross-compile, the window-control surface is on the Platform trait, and the Windows implementation and the window verbs are being written in parallel. | | 2026-09-11 | Phase 3 landed in code: the Windows layer (window enumeration with hung detection, process queries, background spawn with SW_SHOWNOACTIVATE, posted keys and clicks, WM_SETTEXT, native menu walk plus WM_COMMAND, button clicks through BM_CLICK with a deadline then UI Automation, in-process UIA invoke and set-value, foreground push and the one sanctioned bring-to-front with a measured result, placement, PrintWindow capture for the canvas probe, idle time), every cross-process call gated on a WM_NULL probe so a hung KiCad can never hang the bridge; the ab callback client with forwarded caller identity; the KiCad window model and dialog tables; and the 20 window verbs (launch, the open family with the rungs recorded as `mechanism`, the close family with discardChanges through the save prompt, dismiss_dialogs, window_info, state, screenshot_all through ab, send_key, click, place_footprint, enable_software_opengl). Progress dialogs are never dismissed; SendInput is not in the native build at all. 79 verbs, 160 tests, exe 3.5 MB. Live verification of phases 2 and 3 waits on ConfRoomROG, which has been off the relay since the phase 2 binary was resumed there; winvm came back on its own, so the box needs a look. | | 2026-09-11 | Phases 2 and 3 verified live. On arav-rog (KiCad 10.0.3): live routing state in 15 ms, dry run with snapshot DRC, commits as native undo steps, stale-revision refusal, vias, a crossing route rejected by DRC with nothing landed, removal verified, and both regression demo modes at zero errors and zero unconnected (the Python gate's numbers). On ConfRoomROG (KiCad 10.0.5) the full verb runner against the phase 3 binary: 61 pass, 18 fail, 16 skip, with every window verb passing and zero foreground steals. The 18 failures were verbs the binary did not carry yet (show family, progress, verb_times, upgrade, demo, the retired plugin verbs, the export_gerbers alias) plus one runner ordering bug, all addressed the same day: alias added, plugin verbs answer `plugin_retired` with the replacement verb, runner phases fixed, show and upgrade ports in progress. The server became multithreaded (six workers, a GUI lock serialising window and IPC verbs) so status and progress polls answer in tens of milliseconds during a long verb. Two facts learned on the boxes: KiCad runs one API server per machine (first process wins; a standalone PCB editor beside the project manager gets none), now reported as `no_pcb_frame` with the remedy; and ab spawned a legacy `bridges\kicad` folder (bridge 0.9.65) on both boxes while its registry claimed the cache build, filed as [adom/adom-bridge#186](https://wiki.adom.inc/adom/adom-bridge/issues/186) and moved aside on both. |+| 2026-09-11 | Phase 3 complete: alpha.3 (94 verbs: show family, progress registry with `kicad_progress`, `kicad_verb_times` and the `operations` block on status, `kicad_upgrade` with the silent NSIS install behind the platform trait, `kicad_rescan_libraries`, the retired plugin verbs answering honestly). Full verb runner on ConfRoomROG against the native binary: 72 pass, 3 fail, 15 skip, zero foreground steals; the Python baseline on the same box was 72, 1, 15. The three failures: `kicad_demo` (phase 5) and the two retired plugin verbs, which returned an honest error where the runner expects a status (now they answer success with `retired:true`). 180 tests. Phase 4 (etiquette loop) and phase 5 (tour) start next, on a trait surface fixed in advance so the two run in parallel. |  ## The goal in one sentence 
rust/crates/kicad-bridge/src/groups.rs+1
@@ -25,6 +25,7 @@ pub static GROUPS: &[Group] = &[     Group { name: "routing", verbs: || crate::verbs_routing::VERBS, dispatch: crate::verbs_routing::dispatch },     Group { name: "windows", verbs: || crate::verbs_windows::VERBS, dispatch: crate::verbs_windows::dispatch },     Group { name: "show", verbs: || crate::verbs_show::VERBS, dispatch: crate::verbs_show::dispatch },+    Group { name: "demo", verbs: || crate::verbs_demo::VERBS, dispatch: crate::verbs_demo::dispatch }, ];  pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> {
rust/crates/kicad-bridge/src/main.rs+1
@@ -11,6 +11,7 @@ mod catalog; mod groups; mod util; mod verbs;+mod verbs_demo; mod verbs_install; mod verbs_libraries; mod verbs_lint;
rust/crates/kicad-bridge/src/verbs_demo.rsadded+11
@@ -0,0 +1,11 @@+//! Verb group "demo". Placeholder until phase 5 lands.+use serde_json::Value;++use crate::catalog::Verb;+use crate::util::State;++pub static VERBS: &[Verb] = &[];++pub fn dispatch(_state: &mut State, _command: &str, _args: &Value) -> Option<Value> {+    None+}
rust/crates/kicad-bridge/src/verbs_misc.rs+8−2
@@ -100,8 +100,14 @@ pub static VERBS: &[Verb] = &[  pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> {     Some(match command {-        "kicad_bridge_status" => retired("bridge_status", None),-        "kicad_bridge_call" => retired("bridge_call", args.get("method").and_then(Value::as_str)),+        "kicad_bridge_status" => json!({"success": true, "retired": true, "deployed": false, "alive": 0, "plugins": [],+            "output": "no in-KiCad plugin: the native bridge drives KiCad through the IPC API, the native menu bar and UI Automation",+            "_hint": "Nothing to deploy or probe. Editor opening uses the menu walk and UIA, board state uses kicad_routing_state, rescans use kicad_rescan_libraries."}),+        "kicad_bridge_call" => match args.get("method").and_then(Value::as_str).unwrap_or("") {+            "" | "ping" | "status" | "health" => json!({"success": true, "retired": true, "alive": false, "output": "pong from the native bridge; there is no in-KiCad plugin to reach",+                "_hint": "The plugin is retired. Use the verb that does the job directly (kicad_describe lists them)."}),+            m => retired("bridge_call", Some(m)),+        },         "kicad_install_plugin" => json!({"success": true, "installed": false, "retired": true, "output": "nothing to install: the native bridge has no in-KiCad plugin", "_hint": "Editor opening, board state and rescans no longer need a plugin. kicad_uninstall removes any payload the Python bridge left behind."}),         "kicad_plugin_diagnose" => plugin_diagnose(),         "kicad_ipc_api" => ipc_api(args),
rust/crates/kicad-core/src/demo.rsadded+1
@@ -0,0 +1 @@+//! Placeholder: the six-beat demo tour, filled in by phase 5.
rust/crates/kicad-core/src/lib.rs+1
@@ -22,3 +22,4 @@ pub mod ab; pub mod windows_model; pub mod progress; pub mod install_kicad;+pub mod demo;
rust/crates/kicad-platform/src/lib.rs+38
@@ -212,6 +212,44 @@ pub trait Platform: Sync + Send {     /// The official downloads.kicad.org installer for `version` on this OS, or None     /// when installs go through the OS package path instead (macOS, Linux).     fn installer_download_url(&self, _version: &str) -> Option<String> { None }++    // ---- Phase 4: the etiquette loop (see docs/rust-port-plan.md "Phase 4 specification").+    // Defaults do nothing, so an OS without the loop still runs every verb; the Windows+    // implementation makes them real.++    /// Start the sentinel and event-hook threads once. Idempotent.+    fn etiquette_start(&self) {}+    /// A window verb begins (arms the guard, snapshots the park baseline). `spawns` = the+    /// verb opens a new window and gets the long guard.+    fn etiquette_begin_verb(&self, _verb: &str, _spawns: bool) {}+    fn etiquette_end_verb(&self, _verb: &str) {}+    /// Record a KiCad process (and its children) as bridge-owned, persisted to disk.+    fn etiquette_register_pid(&self, _pid: u32, _project: Option<&Path>) {}+    /// A foreground the bridge itself sanctioned (bring_to_front with a reason): the loop+    /// ignores this window for `seconds`.+    fn etiquette_sanction(&self, _hwnd: u64, _seconds: f64) {}+    /// Recent focus transitions with their classification (the Python's focusEvents).+    fn etiquette_events(&self) -> Vec<Value> { Vec::new() }+    /// The Python's focusDebug block.+    fn etiquette_debug(&self) -> Value { Value::Null }+    /// Is this hwnd bridge-owned according to the ledger? None when there is no ledger.+    fn etiquette_owned(&self, _hwnd: u64) -> Option<bool> { None }++    // ---- Phase 5: real input for the demo tour only. Every call must refuse unless+    // `hwnd` is the measured foreground window (the tour foregrounds its beat once, with a+    // reason); nothing else in the bridge may synthesise input.++    /// Move the cursor to screen coordinates (absolute, virtual desktop).+    fn tour_mouse_move(&self, _hwnd: u64, _x: i32, _y: i32) -> Result<(), String> { nope("tour_mouse_move") }+    /// Press or release a mouse button: "left" | "middle" | "right".+    fn tour_mouse_button(&self, _hwnd: u64, _button: &str, _down: bool) -> Result<(), String> { nope("tour_mouse_button") }+    /// Wheel clicks, positive = up.+    fn tour_wheel(&self, _hwnd: u64, _clicks: i32) -> Result<(), String> { nope("tour_wheel") }+    /// Press and release a virtual key.+    fn tour_key(&self, _hwnd: u64, _vk: u16) -> Result<(), String> { nope("tour_key") }+    /// Play an audio file without a window (narration). Returns a handle for tour_audio_stop.+    fn tour_audio_play(&self, _path: &Path, _volume: f32) -> Result<u64, String> { nope("tour_audio_play") }+    fn tour_audio_stop(&self, _handle: u64) -> Result<(), String> { nope("tour_audio_stop") } }  /// Standard "this build cannot do that here" reply.