← All Pull Requests

kicad-lane: 1.0.63, the EDA Bridge Standard's shapes for KiCad's 47 standard verbs #24

Merged opened by Colby Knox 2026-10-27

From kicad-lane. KiCad bridge 1.0.63 takes the EDA Bridge Standard's shapes for the 47 verbs KiCad brought to the standard (standard 0.3.0). KiCad's own names and arguments keep working.

  • std_shapes.rs (new): one layer between the call and the verb.
    • sheet and project paths;
    • id/ids for uuids;
    • mm-named arguments;
    • list forms as one kicad_sch_batch write;
    • frame:"standard" for y up (sheet replies come back y up; board replies stay in KiCad's frame);
    • kicad_edit_footprint with library arguments goes to kicad_fp_edit;
    • sch_pins also answers a flat pins list;
    • describe marks the verbs with standardShape.
  • catalog.rs: 8 standard-name aliases: render_board_3d, remove_parts, edit_parts, align_parts, remove_graphics, read_footprint, export_library_part, create_part_project. standard_names.txt is re-synced from standard 0.3.0, and 47 verbs move out of kicad_only.txt.
  • board_edit.rs, board_live.rs: edits: [{ref, ...}] gives each part its own edit in one undo step, on the file and live.
  • verbs_fab.rs: estimate_cost names its price source and lists prices. Its assumptions and hint say the rates are built in and were never checked against the fab's page.
  • install.rs: create_part_project refuses an existing project unless overwrite:true.
  • tools/build_native_release.py: in the same push as the reference, it drops any exact KiCad equivalent the release now answers by name through an alias.
  • Battery: phase 9 runs the standard shapes on KiCad 10.0.7.
  • Docs: the edits skill, the dev and publish skills, and rust/README.md.

Tests:

  • cargo test --workspace: every test passes except the 7 known schematic_edit tests (their fixture was never pushed), with 6 new standard-shape tests.
  • Verb battery on DESKTOP-BAR6S0F (KiCad 10.0.7), signed dev build: 242 pass, 0 fail, 1 skip; coverage complete; 0 foreground grabs.
  • Phase 9 alone, after the last change: 31 pass.

🤖 Generated with Claude Code

Diff Skip to comments

--- a/bridge.json+++ b/bridge.json@@ -1,350 +1,359 @@⋯ 244 unchanged lines ⋯     "kicad_fab_check",     "kicad_find_symbol",     "kicad_export_manufacturing",-    "kicad_add_rule"+    "kicad_add_rule",+    "kicad_render_board_3d",+    "kicad_remove_parts",+    "kicad_edit_parts",+    "kicad_align_parts",+    "kicad_remove_graphics",+    "kicad_read_footprint",+    "kicad_export_library_part",+    "kicad_create_part_project"   ],   "statusVerb": "kicad_status",   "timeouts": {⋯ 96 unchanged lines ⋯   "releasedAt": "2026-06-26T16:00:00Z",   "uninstall": "kicad_uninstall" }+--- a/rust/Cargo.lock+++ b/rust/Cargo.lock@@ -1,1297 +1,1298 @@⋯ 309 unchanged lines ⋯  [[package]] name = "kicad-bridge"-version = "1.0.62"+version = "1.0.63" dependencies = [  "kicad-core",  "kicad-platform",⋯ 4 unchanged lines ⋯  [[package]] name = "kicad-core"-version = "1.0.62"+version = "1.0.63" dependencies = [  "kicad-ipc-rs",  "kicad-platform",⋯ 20 unchanged lines ⋯  [[package]] name = "kicad-platform"-version = "1.0.62"+version = "1.0.63" dependencies = [  "serde",  "serde_json",⋯ 940 unchanged lines ⋯ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"+--- a/rust/Cargo.toml+++ b/rust/Cargo.toml@@ -1,27 +1,28 @@⋯ 2 unchanged lines ⋯ members = ["crates/*"]  [workspace.package]-version = "1.0.62"+version = "1.0.63" edition = "2021" license = "MIT" publish = false⋯ 15 unchanged lines ⋯ codegen-units = 1 strip = true panic = "abort"+--- a/rust/README.md+++ b/rust/README.md@@ -1,59 +1,63 @@⋯ 41 unchanged lines ⋯ - **The standard has the job**: name the verb after it (`kicad_net_classes`, not `kicad_netclasses`), or keep your   name and add `(standard name, your verb)` to `catalog::ALIASES`, so both names are served. - **It covers part of the job**: keep your name, list the verb in `kicad_only.txt`, and record it as partial in the-  standard's `tools/equivalents.json`.-- **The standard has no such job**: list the verb in `rust/crates/kicad-bridge/src/kicad_only.txt`.+  standard's `tools/equivalents/kicad.json`.+- **The standard has no such job**: list the verb in `rust/crates/kicad-bridge/src/kicad_only.txt`. If other EDA+  tools could do the job, propose it to the standard (an issue titled `kicad-lane: propose ...` on its page; once+  the standard-lane thread agrees, the entry goes in `tools/proposed/kicad-lane.json`). When it lands, take the+  verb out of `kicad_only.txt` and give it the standard's arguments in `std_shapes.rs`.  `catalog.rs`'s test `every_verb_has_a_standard_decision` fails until every verb is one of the three, and `bridge_json_declares_exactly_the_catalog` fails until `bridge.json` lists it. Each new verb also needs an entry in⋯ 7 unchanged lines ⋯ 2. Add the match arm in `verbs.rs::dispatch`. 3. Add it to `bridge.json` and give it an entry in the verb battery (`skills/kicad-bridge-test/run_verb_tests.py`). 4. Keep the Python contract: same arg names, same `success`/`output`/`data` shape, so the verb runner in `skills/kicad-bridge-test` passes unchanged.+--- a/rust/crates/kicad-bridge/src/catalog.rs+++ b/rust/crates/kicad-bridge/src/catalog.rs@@ -1,349 +1,363 @@⋯ 203 unchanged lines ⋯     ("kicad_find_symbol", "kicad_search_library"),     ("kicad_export_manufacturing", "kicad_export_fab_package"),     ("kicad_add_rule", "kicad_custom_rules"),+    // standard 0.3.0: jobs KiCad proposed under the standard's names (kicad_edit_footprint with+    // library arguments goes to kicad_fp_edit in std_shapes.rs: that name is already a KiCad verb)+    ("kicad_render_board_3d", "kicad_render_board"),+    ("kicad_remove_parts", "kicad_delete_footprint"),+    ("kicad_edit_parts", "kicad_edit_footprint"),+    ("kicad_align_parts", "kicad_align_footprints"),+    ("kicad_remove_graphics", "kicad_delete_graphics"),+    ("kicad_read_footprint", "kicad_fp_info"),+    ("kicad_export_library_part", "kicad_export_part"),+    ("kicad_create_part_project", "kicad_make_part_project"), ];  /// The verb a standard name stands for, when it is an alias.⋯ 64 unchanged lines ⋯             if let Some(t) = alias_target(v.name) {                 e["aliasOf"] = json!(t);             }+            if crate::std_shapes::accepts(alias_target(v.name).unwrap_or(v.name)) {+                e["standardShape"] = json!(crate::std_shapes::SHAPE_NOTE);+            }             e         })         .collect();⋯ 67 unchanged lines ⋯         assert_eq!(doc["prewarm"]["verb"], "kicad_prewarm");     } }+--- a/rust/crates/kicad-bridge/src/kicad_only.txt+++ b/rust/crates/kicad-bridge/src/kicad_only.txt@@ -1,156 +1,110 @@⋯ 11 unchanged lines ⋯ kicad_add_zone kicad_adom_library_heal kicad_adom_library_status-kicad_align_footprints kicad_analyze_connections kicad_apply_zone_plan kicad_board_pads kicad_board_rules-kicad_bodies kicad_bridge_call kicad_bridge_status-kicad_check_clearance kicad_check_for_updates kicad_click kicad_close_3d_viewer kicad_close_footprint_editor kicad_close_symbol_editor-kicad_copy_routing-kicad_cross_probe-kicad_delete_footprint-kicad_delete_graphics kicad_design_review-kicad_edit_footprint kicad_enable_software_opengl kicad_errors-kicad_estimate_cost kicad_export_bom_csv-kicad_export_dxf-kicad_export_gencad-kicad_export_ipc2581-kicad_export_ipc356 kicad_export_molecule-kicad_export_odb-kicad_export_part kicad_export_pos kicad_export_svg kicad_extract_netlist⋯ 4 unchanged lines ⋯ kicad_footprint_models kicad_format_upgrade kicad_fp_edit-kicad_fp_info kicad_freerouting kicad_get_settings-kicad_import_svg kicad_install_footprint kicad_install_plugin kicad_install_symbol⋯ 6 unchanged lines ⋯ kicad_list_symbols kicad_list_versions kicad_log_tail-kicad_make_part_project kicad_menu_items kicad_model_check kicad_modify_track⋯ 9 unchanged lines ⋯ kicad_pcm_list kicad_pcm_remove_repository kicad_pcm_uninstall-kicad_place_decoupling kicad_place_footprint kicad_placement_state kicad_placement_validate-kicad_plan_fanout kicad_plugin_diagnose kicad_prewarm-kicad_project_create-kicad_project_info-kicad_project_rename-kicad_project_snapshot kicad_refresh_board_views kicad_remove_route kicad_remove_zone-kicad_render_board-kicad_render_schematic kicad_rescan_libraries kicad_route kicad_routing_validate kicad_sch_batch-kicad_sch_bus-kicad_sch_bus_entry kicad_sch_check kicad_sch_components-kicad_sch_connect_pins-kicad_sch_connect_to_bus kicad_sch_connect_to_net-kicad_sch_create kicad_sch_delete-kicad_sch_delete_wire-kicad_sch_edit_label-kicad_sch_fix_connectivity-kicad_sch_hierarchy-kicad_sch_items-kicad_sch_junction kicad_sch_label kicad_sch_move-kicad_sch_move_region kicad_sch_no_connect-kicad_sch_page-kicad_sch_pins kicad_sch_place_symbol kicad_sch_replace_symbol-kicad_sch_reset_fields kicad_sch_rotate kicad_sch_set_property-kicad_sch_sheet_pins-kicad_sch_split_wire-kicad_sch_text-kicad_sch_trace_point-kicad_sch_update_from_library kicad_sch_wire kicad_score_placement kicad_screenshot_all kicad_select_clear kicad_select_items kicad_send_key-kicad_set_layer_count kicad_set_settings kicad_silk_fields_batch kicad_silk_graphics_batch⋯ 5 unchanged lines ⋯ kicad_upgrade kicad_verb_times kicad_zone_state+--- a/rust/crates/kicad-bridge/src/main.rs+++ b/rust/crates/kicad-bridge/src/main.rs@@ -1,228 +1,230 @@⋯ 33 unchanged lines ⋯ mod verbs_sch_hier; mod verbs_show; mod verbs_standard;+mod std_shapes; mod verbs_windows;  use std::sync::atomic::{AtomicU64, Ordering};⋯ 186 unchanged lines ⋯     };     let _ = req.respond(json_response(body, code)); }+--- a/rust/crates/kicad-bridge/src/standard_names.txt+++ b/rust/crates/kicad-bridge/src/standard_names.txt@@ -1,202 +1,257 @@-# The EDA Bridge Standard's verb names (adom/eda-bridge-standard verbs/standard.json, reference altium 0.2.75). Generated by tools/sync_standard_names.py; do not edit.+# The EDA Bridge Standard's verb names (adom/eda-bridge-standard verbs/standard.json, reference altium 0.2.76). Generated by tools/sync_standard_names.py; do not edit. a365_configure a365_files_upload a365_library_overview⋯ 14 unchanged lines ⋯ agent_start agent_status agent_stop+align_parts animate_view annotate apply_pours⋯ 6 unchanged lines ⋯ board_report board_shape board_summary+bodies bom build_board build_footprint build_schematic build_symbol_modes+check_clearance check_dialogs check_orientation cleanup_libs clone_symbol close+close_all_documents close_document compile compile_intlib⋯ 4 unchanged lines ⋯ copper_balance copper_layers copy_footprint+copy_routing create_footprint+create_part_project create_symbol cross_check+cross_probe current_capacity demo describe⋯ 2 unchanged lines ⋯ diff_pairs dismiss_dialogs doc_params+document_info+edit_footprint+edit_parts ensure_licensed+estimate_cost eval export_bom+export_dxf+export_gencad export_gerber+export_ipc2581+export_ipc356+export_library_part export_manufacturing+export_odb export_pdf export_step extension_ping⋯ 4 unchanged lines ⋯ footprint_from_land_pattern footprint_primitives foreground+generate_body height_check impedance import_kicad import_part+import_svg install install_extension install_library⋯ 6 unchanged lines ⋯ layers layers_in_use length_tune+library_views license license_status list_commands⋯ 15 unchanged lines ⋯ part_parameters pcb_report pin_pad_check+place_decoupling place_on_pcb place_on_sch place_vias placement_check+plan_fanout+plan_route plate_vias pour_state progress+project_create+project_info+project_rename+project_snapshot read_board+read_footprint read_rules read_schematic read_sheet_file read_symbol readiness remove_footprint+remove_graphics remove_items+remove_parts remove_pours remove_symbol rename_footprint rename_symbol+render_board_3d+render_board_layers+render_schematic replicate_layout repour return_path⋯ 10 unchanged lines ⋯ run_outjob run_script save+sch_bus+sch_bus_entry sch_connect+sch_connect_pins+sch_connect_to_bus+sch_create+sch_delete_wire sch_edit+sch_edit_label+sch_fix_connectivity+sch_hierarchy+sch_items+sch_junction+sch_move_region+sch_page+sch_pins sch_place_part+sch_reset_fields+sch_sheet_pins sch_sheet_symbol+sch_split_wire sch_swap_part+sch_text+sch_trace_point+sch_update_from_library sch_zoom schematic_data schematic_review⋯ 1 unchanged line ⋯ selection selftest_script_error set_design_rules+set_layer_count set_layer_visibility set_models set_outline⋯ 33 unchanged lines ⋯ workspace_search xcopy_footprint zoom_to+--- a/rust/crates/kicad-bridge/src/std_shapes.rs+++ b/rust/crates/kicad-bridge/src/std_shapes.rs@@ -0,0 +1,645 @@+//! The EDA Bridge Standard's argument shapes for the verbs this bridge proposed to it (standard 0.2.0+//! and 0.3.0: the schematic wiring verbs, the extra exports, the board edits, projects and library+//! footprints). Each verb keeps its KiCad arguments; this module also accepts the standard's, written+//! in `tools/proposed/kicad-lane.json` of adom/eda-bridge-standard:+//!+//! - `sheet` and `project` (paths) for `filePath` and `projectPath`, `id`/`ids` for KiCad's uuids,+//!   unit-named numbers (`toleranceMm`, `gapMm` ...) for KiCad's bare ones;+//! - a list of edits (`pairs`, `entries`, `edits`, `texts`, junction `remove`) runs as one+//!   `kicad_sch_batch`: one read, one atomic write, all or nothing;+//! - `frame:"standard"` takes coordinates with y up (KiCad's y negated). On a sheet verb the reply's+//!   points come back in the same frame; a board verb's reply stays in KiCad's frame, as for the+//!   other board edit verbs, and `kicad_routing_state {model:"standard"}` reads the result.+//!+//! Replies gain `id` beside every `uuid` (and `ids` beside `uuids`).++use serde_json::{json, Map, Value};++use crate::util::fail;++/// A call rewritten into KiCad's own shape, and what the reply needs on the way back.+pub struct Shaped {+    pub command: String,+    pub args: Value,+    /// the sheet verb's points come back with y up+    flip_reply: bool,+    /// the call ran as a kicad_sch_batch of this many edits: merge their results+    batched: bool,+    /// the board verb took the standard frame (its reply stays in KiCad's)+    board_frame: bool,+}++/// The verbs whose standard shape this module knows: the bridge's own name, after aliases.+const SHEET_VERBS: &[&str] = &[+    "kicad_sch_connect_pins", "kicad_sch_connect_to_bus", "kicad_sch_junction", "kicad_sch_bus", "kicad_sch_bus_entry",+    "kicad_sch_split_wire", "kicad_sch_delete_wire", "kicad_sch_edit_label", "kicad_sch_move_region", "kicad_sch_text",+    "kicad_sch_page", "kicad_sch_items", "kicad_sch_pins", "kicad_sch_trace_point", "kicad_sch_hierarchy",+    "kicad_sch_sheet_pins", "kicad_sch_reset_fields", "kicad_sch_update_from_library", "kicad_sch_fix_connectivity",+    "kicad_render_schematic", "kicad_sch_create",+];+const BOARD_FRAME_VERBS: &[&str] = &["kicad_align_footprints", "kicad_import_svg", "kicad_copy_routing"];+const OTHER_VERBS: &[&str] = &[+    "kicad_export_odb", "kicad_export_ipc2581", "kicad_export_ipc356", "kicad_export_gencad", "kicad_export_dxf",+    "kicad_render_board", "kicad_estimate_cost", "kicad_delete_footprint", "kicad_edit_footprint", "kicad_set_layer_count",+    "kicad_delete_graphics", "kicad_cross_probe", "kicad_check_clearance", "kicad_place_decoupling", "kicad_plan_fanout",+    "kicad_project_create", "kicad_project_info", "kicad_project_rename", "kicad_project_snapshot", "kicad_fp_info",+    "kicad_fp_edit", "kicad_export_part", "kicad_make_part_project",+];++/// What describe says on each verb this module shapes.+pub const SHAPE_NOTE: &str = "Also takes the EDA Bridge Standard's arguments (adom/eda-bridge-standard, tools/proposed/kicad-lane.json): sheet and project paths, id/ids for uuids, mm-named numbers, a list of edits as one write, and frame:\"standard\" for y-up coordinates (a sheet verb's reply then comes back y up too).";++/// True when the verb (KiCad's own name) takes the standard's shape here.+pub fn accepts(command: &str) -> bool {+    SHEET_VERBS.contains(&command) || BOARD_FRAME_VERBS.contains(&command) || OTHER_VERBS.contains(&command)+}++/// Argument renames per verb: (the standard's name, KiCad's). Applied only when KiCad's is absent.+fn renames(command: &str) -> &'static [(&'static str, &'static str)] {+    match command {+        "kicad_sch_split_wire" => &[("id", "uuid")],+        "kicad_sch_delete_wire" => &[("ids", "uuids"), ("id", "uuid")],+        "kicad_sch_page" | "kicad_sch_create" => &[("widthMm", "width"), ("heightMm", "height")],+        "kicad_sch_trace_point" | "kicad_sch_fix_connectivity" => &[("toleranceMm", "tolerance")],+        "kicad_sch_sheet_pins" => &[("kind", "shape")],+        "kicad_sch_update_from_library" => &[("symbols", "libIds"), ("library", "libraryPath")],+        "kicad_export_dxf" => &[("contours", "useContours"), ("titleBlock", "includeBorderTitle"), ("excludeDesignators", "excludeRefdes"), ("excludeValues", "excludeValue")],+        "kicad_align_footprints" | "kicad_place_decoupling" => &[("gapMm", "gap")],+        "kicad_delete_graphics" => &[("ids", "uuids"), ("kinds", "type")],+        "kicad_import_svg" => &[("widthMm", "width")],+        "kicad_cross_probe" => &[("ref", "reference"), ("symbolId", "symbolUuid"), ("schematic", "schematicPath")],+        "kicad_check_clearance" => &[("thresholdMm", "threshold")],+        "kicad_plan_fanout" => &[("viaMm", "viaSize"), ("viaDrillMm", "viaDrill"), ("widthMm", "width"), ("clearanceMm", "clearance"), ("pitchMm", "pitch")],+        "kicad_export_part" | "kicad_make_part_project" => &[("part", "partName"), ("footprint", "footprintName")],+        _ => &[],+    }+}++fn path_like(v: &Value) -> bool {+    v.as_str().map(|s| s.contains('/') || s.contains('\\') || s.to_ascii_lowercase().ends_with(".kicad_sch") || s.to_ascii_lowercase().ends_with(".kicad_pro")).unwrap_or(false)+}++fn rename(o: &mut Map<String, Value>, from: &str, to: &str) {+    if !o.contains_key(to) {+        if let Some(v) = o.remove(from) {+            o.insert(to.into(), v);+        }+    }+}++fn neg(v: &Value) -> Value {+    json!(-v.as_f64().unwrap_or(0.0) + 0.0)+}++/// [x, y] with y negated; anything else left alone (a "U1.3" pin name, a uuid).+fn flip_pt(v: &mut Value) {+    if let Value::Array(a) = v {+        if a.len() == 2 && a[0].is_number() && a[1].is_number() {+            a[1] = neg(&a[1]);+        }+    }+}++/// A box [x1, y1, x2, y2] in the other frame: y negated, so the two y's swap to stay low-high.+fn flip_box(v: &mut Value) {+    if let Value::Array(a) = v {+        if a.len() == 4 && a.iter().all(Value::is_number) {+            let (y1, y2) = (a[1].clone(), a[3].clone());+            a[1] = neg(&y2);+            a[3] = neg(&y1);+        }+    }+}++/// A sheet verb's arguments (or one of its list items) from y up to KiCad's y down.+fn flip_sheet_args(v: &mut Value) {+    let Some(o) = v.as_object_mut() else { return };+    for k in ["at", "to", "from", "by"] {+        if let Some(p) = o.get_mut(k) {+            flip_pt(p);+        }+    }+    for k in ["points", "via"] {+        if let Some(Value::Array(a)) = o.get_mut(k) {+            a.iter_mut().for_each(flip_pt);+        }+    }+    if let Some(b) = o.get_mut("box") {+        flip_box(b);+    }+    for k in ["pairs", "entries", "edits", "texts", "remove"] {+        if let Some(Value::Array(a)) = o.get_mut(k) {+            for it in a.iter_mut() {+                if it.is_object() {+                    flip_sheet_args(it);+                } else {+                    flip_pt(it);+                }+            }+        }+    }+}++/// A sheet verb's reply from KiCad's y down to y up: every point under a point key, every list+/// of points under a list key, every box. Pin names and uuids are not numbers and stay.+fn flip_reply(v: &mut Value) {+    const PT: &[&str] = &["at", "end", "from", "to", "start", "nearest", "mid", "anchor", "position"];+    const LIST: &[&str] = &["points", "junctionsAdded", "junctionsPruned", "junctionsRemoved", "notNeeded", "unresolved", "skipped", "added", "removed", "dots"];+    const BOX: &[&str] = &["box", "changedRegionMm", "bbox", "bounds"];+    match v {+        Value::Object(o) => {+            for (k, x) in o.iter_mut() {+                if PT.contains(&k.as_str()) {+                    flip_pt(x);+                } else if LIST.contains(&k.as_str()) {+                    if let Value::Array(a) = x {+                        for it in a.iter_mut() {+                            if it.is_array() {+                                flip_pt(it);+                            }+                        }+                    }+                } else if BOX.contains(&k.as_str()) {+                    flip_box(x);+                }+                flip_reply(x);+            }+        }+        Value::Array(a) => a.iter_mut().for_each(flip_reply),+        _ => {}+    }+}++/// `id` beside every `uuid`, `ids` beside every `uuids`, all the way down.+fn add_ids(v: &mut Value) {+    match v {+        Value::Object(o) => {+            if let Some(u) = o.get("uuid").cloned() {+                o.entry("id").or_insert(u);+            }+            if let Some(u) = o.get("uuids").cloned() {+                o.entry("ids").or_insert(u);+            }+            o.values_mut().for_each(add_ids);+        }+        Value::Array(a) => a.iter_mut().for_each(add_ids),+        _ => {}+    }+}++/// The batch ops for a standard list form, or None when the call is not one.+fn batch_ops(command: &str, o: &Map<String, Value>) -> Option<Vec<Value>> {+    let objects = |k: &str| o.get(k).and_then(Value::as_array).filter(|a| !a.is_empty() && a.iter().all(Value::is_object)).cloned();+    let with_op = |items: Vec<Value>, op: &str, f: &dyn Fn(&mut Map<String, Value>)| -> Vec<Value> {+        items+            .into_iter()+            .map(|mut it| {+                if let Some(m) = it.as_object_mut() {+                    f(m);+                    m.insert("op".into(), json!(op));+                }+                it+            })+            .collect()+    };+    match command {+        "kicad_sch_connect_pins" => objects("pairs").map(|a| with_op(a, "connect_pins", &|_| {})),+        "kicad_sch_bus_entry" => objects("entries").map(|a| with_op(a, "bus_entry", &|_| {})),+        "kicad_sch_edit_label" => objects("edits").map(|a| with_op(a, "edit_label", &|m| rename(m, "id", "uuid"))),+        "kicad_sch_text" => objects("texts").map(|a| with_op(a, "text", &|m| rename(m, "sizeMm", "size"))),+        "kicad_sch_junction" => {+            let remove = o.get("remove").and_then(Value::as_array)?;+            let mut ops = Vec::new();+            if let Some(p) = o.get("points").filter(|p| p.as_array().map(|a| !a.is_empty()).unwrap_or(false)) {+                ops.push(json!({"op": "junction", "points": p}));+            }+            let ids: Vec<&Value> = remove.iter().filter(|r| r.is_string()).collect();+            let pts: Vec<&Value> = remove.iter().filter(|r| r.is_array()).collect();+            if !ids.is_empty() || !pts.is_empty() {+                ops.push(json!({"op": "junction", "remove": true, "uuids": ids, "points": pts}));+            }+            Some(ops)+        }+        _ => None,+    }+}++/// `kicad_edit_footprint` is the board verb (edit placed parts: the standard's edit_parts); the+/// standard's `edit_footprint` edits a LIBRARY footprint, which is kicad_fp_edit. A call with+/// library arguments and no part names goes there.+fn is_library_footprint_edit(o: &Map<String, Value>) -> bool {+    let lib = o.contains_key("libraryPath") || o.contains_key("libId") || o.get("filePath").and_then(Value::as_str).map(|p| p.to_ascii_lowercase().ends_with(".kicad_mod")).unwrap_or(false);+    lib && !o.contains_key("ref") && !o.contains_key("refs") && !o.contains_key("edits")+}++/// `edits: [{ref, ...}]` for edit_parts, in KiCad's keys.+fn edit_parts_items(o: &mut Map<String, Value>) {+    if let Some(Value::Array(edits)) = o.get_mut("edits") {+        for e in edits.iter_mut() {+            let Some(m) = e.as_object_mut() else { continue };+            rename(m, "newRef", "newReference");+            rename(m, "excludeFromPos", "excludeFromPosFiles");+            if let Some(mount) = m.remove("mount") {+                let t = match mount.as_str() {+                    Some("smd") => "smd",+                    Some("through_hole") => "through_hole",+                    _ => "unspecified",+                };+                let attrs = m.entry("attributes").or_insert(json!({}));+                if let Some(a) = attrs.as_object_mut() {+                    a.insert("type".into(), json!(t));+                }+            }+        }+    }+}++/// Library footprint edits in KiCad's keys: pad sizes and drills, graphics widths, the mount type.+fn fp_edit_items(o: &mut Map<String, Value>) {+    if let Some(Value::Array(pads)) = o.get_mut("pads") {+        for p in pads.iter_mut() {+            if let Some(s) = p.get_mut("set").and_then(Value::as_object_mut) {+                rename(s, "sizeMm", "size");+                rename(s, "drillMm", "drill");+            }+        }+    }+    if let Some(Value::Array(pads)) = o.get_mut("addPads") {+        for p in pads.iter_mut().filter_map(Value::as_object_mut) {+            rename(p, "sizeMm", "size");+            rename(p, "drillMm", "drill");+        }+    }+    match o.get_mut("graphics") {+        Some(Value::Array(g)) => g.iter_mut().filter_map(Value::as_object_mut).for_each(|m| rename(m, "widthMm", "width")),+        Some(Value::Object(m)) => rename(m, "widthMm", "width"),+        _ => {}+    }+    if let Some(a) = o.get_mut("metadata").and_then(|m| m.get_mut("attributes")).and_then(Value::as_object_mut) {+        rename(a, "mount", "type");+        rename(a, "excludeFromPos", "excludeFromPosFiles");+    }+}++/// The call in KiCad's shape. None when the verb has no standard shape here; Err is a refusal.+/// `called` is the name the caller used (kicad_ prefixed), `command` the verb it resolved to.+pub fn to_native(called: &str, command: &str, args: &Value) -> Option<Result<Shaped, Value>> {+    let mut command = command.to_string();+    let Some(o0) = args.as_object() else { return None };+    if command == "kicad_edit_footprint" && is_library_footprint_edit(o0) {+        command = "kicad_fp_edit".into();+    }+    let c = command.as_str();+    let sheet = SHEET_VERBS.contains(&c);+    let board_frame = BOARD_FRAME_VERBS.contains(&c);+    if !sheet && !board_frame && !OTHER_VERBS.contains(&c) {+        return None;+    }+    let mut a = args.clone();+    let o = a.as_object_mut()?;+    let frame = match o.remove("frame").as_ref().map(|f| f.as_str()) {+        None | Some(Some("kicad")) => false,+        Some(Some("standard")) | Some(Some("yup")) => true,+        Some(other) => {+            return Some(Err(fail("bad_args", format!("frame must be \"standard\" (y up) or \"kicad\" (y down), not {}", other.map(|s| format!("\"{s}\"")).unwrap_or_else(|| "that".into())), "Leave frame out for KiCad's own y-down coordinates.")));+        }+    };++    // sheet and project: the standard's paths. Where KiCad's own `sheet` is a sheet NAME+    // (sch_sheet_pins, render_schematic), only a path moves, and sheetSymbol takes its place.+    if o.get("sheet").map(path_like).unwrap_or(false) && !o.contains_key("filePath") {+        let p = o.remove("sheet").unwrap_or(Value::Null);+        o.insert("filePath".into(), p);+    }+    if c == "kicad_sch_sheet_pins" {+        rename(o, "sheetSymbol", "sheet");+    }+    if o.get("project").map(path_like).unwrap_or(false) && !o.contains_key("projectPath") {+        let p = o.remove("project").unwrap_or(Value::Null);+        o.insert("projectPath".into(), p);+    }+    for (from, to) in renames(c) {+        rename(o, from, to);+    }++    match c {+        "kicad_sch_items" => {+            if let Some(Value::Array(k)) = o.get_mut("kinds") {+                for x in k.iter_mut() {+                    if x.as_str() == Some("powerPorts") {+                        *x = json!("powerSymbols");+                    }+                }+            }+        }+        "kicad_sch_reset_fields" => {+            if let Some(Value::Array(f)) = o.get_mut("fields") {+                for x in f.iter_mut() {+                    match x.as_str().map(str::to_ascii_lowercase).as_deref() {+                        Some("designator") | Some("reference") => *x = json!("Reference"),+                        Some("value") => *x = json!("Value"),+                        _ => {}+                    }+                }+            }+        }+        "kicad_sch_create" => {+            if let Some(or) = o.remove("orientation") {+                o.entry("portrait").or_insert(json!(or.as_str() == Some("portrait")));+            }+        }+        "kicad_export_ipc2581" => {+            if let Some(Value::Object(b)) = o.remove("bomFields") {+                for (from, to) in [("mpn", "bomColMfgPn"), ("manufacturer", "bomColMfg"), ("distributorPn", "bomColDistPn"), ("distributor", "bomColDist")] {+                    if let Some(v) = b.get(from) {+                        o.entry(to).or_insert(v.clone());+                    }+                }+            }+        }+        "kicad_edit_footprint" => edit_parts_items(o),+        "kicad_fp_edit" => fp_edit_items(o),+        "kicad_make_part_project" if called.ends_with("create_part_project") => {+            // the standard never overwrites without being told; kicad_make_part_project regenerates in place+            if o.get("overwrite").and_then(Value::as_bool) != Some(true) {+                o.insert("_refuseExisting".into(), json!(true));+            }+        }+        _ => {}+    }++    if frame && sheet {+        flip_sheet_args(&mut a);+    }+    let o = a.as_object_mut()?;+    if frame && board_frame {+        match c {+            "kicad_align_footprints" => {+                let ymode = matches!(o.get("mode").and_then(Value::as_str), Some("top") | Some("bottom") | Some("centerY"));+                if ymode {+                    if let Some(t) = o.get("to").cloned() {+                        o.insert("to".into(), neg(&t));+                    }+                }+            }+            "kicad_import_svg" => {+                if let Some(p) = o.get_mut("at") {+                    flip_pt(p);+                }+            }+            "kicad_copy_routing" => {+                if let Some(b) = o.get_mut("box") {+                    flip_box(b);+                }+                if let Some(p) = o.get_mut("offset") {+                    flip_pt(p);+                }+            }+            _ => {}+        }+    }+    // board shapes KiCad spells differently+    match c {+        "kicad_import_svg" => {+            if let Some(Value::Array(p)) = o.get("at").cloned() {+                if p.len() == 2 {+                    o.entry("x").or_insert(p[0].clone());+                    o.entry("y").or_insert(p[1].clone());+                    o.remove("at");+                }+            }+        }+        "kicad_copy_routing" => {+            if !o.contains_key("source") {+                if let Some(Value::Array(b)) = o.remove("box") {+                    if b.len() == 4 {+                        let f = |i: usize| b[i].as_f64().unwrap_or(0.0);+                        o.insert("source".into(), json!({"minX": f(0).min(f(2)), "minY": f(1).min(f(3)), "maxX": f(0).max(f(2)), "maxY": f(1).max(f(3))}));+                    }+                }+            }+        }+        _ => {}+    }++    // a list of edits: one kicad_sch_batch, one write+    let mut batched = false;+    if sheet {+        if let Some(ops) = batch_ops(c, o) {+            let mut b = Map::new();+            for k in ["filePath", "dryRun", "backup"] {+                if let Some(v) = o.get(k) {+                    b.insert(k.into(), v.clone());+                }+            }+            b.insert("ops".into(), Value::Array(ops));+            a = Value::Object(b);+            command = "kicad_sch_batch".into();+            batched = true;+        }+    }+    Some(Ok(Shaped { command, args: a, flip_reply: frame && sheet, batched, board_frame: frame && board_frame }))+}++/// Merge each batched edit's result into the reply's top level: lists join, counts add up.+fn merge_batch(out: &mut Value) {+    let Some(results) = out.get("results").and_then(Value::as_array).cloned() else { return };+    let mut merged = Map::new();+    for r in &results {+        let Some(m) = r.get("result").and_then(Value::as_object) else { continue };+        for (k, v) in m {+            match (merged.get_mut(k), v) {+                (Some(Value::Array(acc)), Value::Array(more)) => acc.extend(more.iter().cloned()),+                (Some(acc), Value::Number(n)) if acc.is_number() => *acc = json!(acc.as_f64().unwrap_or(0.0) + n.as_f64().unwrap_or(0.0)),+                (Some(acc), _) => *acc = v.clone(),+                (None, _) => {+                    merged.insert(k.clone(), v.clone());+                }+            }+        }+    }+    if let Some(o) = out.as_object_mut() {+        for (k, v) in merged {+            o.entry(k).or_insert(v);+        }+        o.insert("batchedAs".into(), json!("kicad_sch_batch"));+    }+}++/// The reply in the standard's terms (ids, the frame, the batch merged, the 3D image's path).+pub fn from_native(s: &Shaped, out: &mut Value) {+    if s.batched {+        merge_batch(out);+    }+    match s.command.as_str() {+        "kicad_render_board" => {+            if let Some(p) = out.pointer("/data/output_path").cloned() {+                out["image"] = p;+            }+        }+        "kicad_make_part_project" => {+            for (std, own) in [("project", "projectFile"), ("schematic", "schematicFile"), ("board", "boardFile")] {+                if let Some(v) = out.get(own).cloned() {+                    out[std] = v;+                }+            }+        }+        "kicad_sch_pins" => {+            // the standard answers one flat list, each pin naming its part+            let flat: Vec<Value> = out.get("symbols").and_then(Value::as_array).map(|syms| {+                syms.iter().flat_map(|s| {+                    let r = s.get("reference").or_else(|| s.get("ref")).cloned().unwrap_or(Value::Null);+                    s.get("pins").and_then(Value::as_array).cloned().unwrap_or_default().into_iter().map(move |mut p| {+                        p["ref"] = r.clone();+                        p+                    })+                }).collect()+            }).unwrap_or_default();+            if out.get("pins").is_none() {+                out["pins"] = json!(flat);+            }+        }+        "kicad_sch_items" => {+            if let Some(p) = out.get("powerSymbols").cloned() {+                out["powerPorts"] = p;+            }+        }+        _ => {}+    }+    add_ids(out);+    if s.flip_reply {+        flip_reply(out);+        out["frame"] = json!("standard");+    }+    if s.board_frame {+        out["inputFrame"] = json!("standard");+        out["_frameNote"] = json!("Your coordinates were taken in the standard's y-up frame; positions in this reply are KiCad's own (y down). kicad_routing_state {model:\"standard\"} reads the result back in the standard's frame.");+    }+}++#[cfg(test)]+mod tests {+    //! The standard's shapes through the real dispatch path, on temp copies of the KiCad 10 fixtures.+    use super::*;+    use crate::util::{norm, State};+    use std::path::{Path, PathBuf};++    fn fixtures() -> PathBuf {+        Path::new(env!("CARGO_MANIFEST_DIR")).join("../kicad-core/tests/fixtures")+    }++    fn temp_copy(tag: &str, rel: &str) -> PathBuf {+        let d = std::env::temp_dir().join(format!("kicad-bridge-std-{tag}-{}-{}", std::process::id(), kicad_core::schematic::new_uuid()));+        std::fs::create_dir_all(&d).unwrap();+        let src = fixtures().join(rel);+        let p = d.join(src.file_name().unwrap());+        std::fs::copy(&src, &p).unwrap();+        p+    }++    fn call(verb: &str, args: Value) -> Value {+        let caller = crate::Caller { thread: "std-shapes-test".into(), container: String::new(), reason: String::new() };+        crate::verbs::dispatch(&mut State::new(), verb, &args, &caller)+    }++    fn ok(verb: &str, args: Value) -> Value {+        let v = call(verb, args);+        assert_eq!(v["success"], true, "{verb}: {v}");+        v+    }++    #[test]+    fn a_list_of_pairs_is_one_batched_write() {+        let p = temp_copy("pairs", "sch-edit/edit10.kicad_sch");+        let before = std::fs::read_to_string(&p).unwrap();+        let args = json!({"sheet": norm(&p), "pairs": [{"from": "R1.2", "to": "C1.1"}, {"from": "R2.2", "to": "C1.2", "first": "v"}]});+        let mut dry = args.clone();+        dry["dryRun"] = json!(true);+        let v = ok("sch_connect_pins", dry);+        assert_eq!(v["batchedAs"], "kicad_sch_batch", "{v}");+        assert_eq!(std::fs::read_to_string(&p).unwrap(), before, "a dry run writes nothing");+        let v = ok("kicad_sch_connect_pins", args);+        assert_eq!(v["results"].as_array().unwrap().len(), 2, "{v}");+        assert!(v["routes"].as_array().map(|r| r.len() >= 2).unwrap_or(false), "routes merged from both edits: {v}");+        assert_ne!(std::fs::read_to_string(&p).unwrap(), before);+    }++    #[test]+    fn texts_and_items_round_trip_in_the_standard_frame() {+        let p = temp_copy("frame", "sch-edit/edit10.kicad_sch");+        ok("sch_text", json!({"sheet": norm(&p), "frame": "standard", "texts": [{"text": "up here", "at": [50.8, -30.48], "sizeMm": 2.54}]}));+        // KiCad's own frame has the text at y = +30.48+        let k = ok("sch_items", json!({"sheet": norm(&p), "kinds": ["texts"]}));+        let t = k["texts"].as_array().unwrap().iter().find(|t| t["text"] == "up here").cloned().unwrap_or_else(|| panic!("{k}"));+        assert_eq!(t["at"][1].as_f64(), Some(30.48), "{t}");+        assert!(t.get("id").is_some(), "id beside uuid: {t}");+        // the standard frame gives back what went in+        let s = ok("sch_items", json!({"sheet": norm(&p), "kinds": ["texts"], "frame": "standard"}));+        assert_eq!(s["frame"], "standard");+        let t = s["texts"].as_array().unwrap().iter().find(|t| t["text"] == "up here").cloned().unwrap();+        assert_eq!(t["at"][1].as_f64(), Some(-30.48), "{t}");+        let pins = ok("sch_pins", json!({"sheet": norm(&p), "references": ["R1"], "frame": "standard"}));+        assert!(pins["pins"].as_array().map(|a| !a.is_empty() && a.iter().all(|x| x["ref"] == "R1")).unwrap_or(false), "{pins}");+    }++    #[test]+    fn junctions_add_and_remove_in_one_call() {+        let p = temp_copy("junction", "sch-edit/edit10.kicad_sch");+        ok("sch_junction", json!({"sheet": norm(&p), "points": [[110.49, 60.96], [120.65, 60.96]]}));+        let items = ok("sch_items", json!({"sheet": norm(&p), "kinds": ["junctions"]}));+        let id = items["junctions"].as_array().unwrap().iter().find(|j| j["at"] == json!([110.49, 60.96])).and_then(|j| j["id"].as_str()).map(String::from).unwrap_or_else(|| panic!("{items}"));+        let v = ok("sch_junction", json!({"sheet": norm(&p), "points": [[130.81, 60.96]], "remove": [id, [120.65, 60.96]]}));+        assert_eq!(v["batchedAs"], "kicad_sch_batch");+        let after = ok("sch_items", json!({"sheet": norm(&p), "kinds": ["junctions"]}));+        let at: Vec<Value> = after["junctions"].as_array().unwrap().iter().map(|j| j["at"].clone()).collect();+        assert!(at.contains(&json!([130.81, 60.96])), "{at:?}");+        assert!(!at.contains(&json!([110.49, 60.96])) && !at.contains(&json!([120.65, 60.96])), "{at:?}");+    }++    #[test]+    fn edit_parts_gives_each_part_its_own_edit() {+        let p = temp_copy("parts", "konnect-route/route-fixture.kicad_pcb");+        let v = ok("edit_parts", json!({"filePath": norm(&p), "source": "file", "edits": [{"ref": "R1", "value": "4k7"}, {"ref": "C1", "value": "10u", "dnp": true}]}));+        let text = std::fs::read_to_string(&p).unwrap();+        assert!(text.contains("\"4k7\"") && text.contains("\"10u\""), "{v}");+        assert_eq!(v["edited"].as_array().map(|e| e.len()), Some(2), "{v}");+        let e = call("edit_parts", json!({"filePath": norm(&p), "source": "file", "edits": [{"ref": "R1", "value": "1k"}, {"ref": "r1", "dnp": true}]}));+        assert_eq!(e["success"], false, "a part edited twice is refused: {e}");+    }++    #[test]+    fn create_part_project_never_overwrites_unasked() {+        let s = to_native("kicad_create_part_project", "kicad_make_part_project", &json!({"part": "R_0603"})).unwrap().unwrap();+        assert_eq!(s.args["partName"], "R_0603");+        assert_eq!(s.args["_refuseExisting"], true);+        let s = to_native("kicad_create_part_project", "kicad_make_part_project", &json!({"part": "R_0603", "overwrite": true})).unwrap().unwrap();+        assert!(s.args.get("_refuseExisting").is_none());+        let s = to_native("kicad_make_part_project", "kicad_make_part_project", &json!({"partName": "R_0603"})).unwrap().unwrap();+        assert!(s.args.get("_refuseExisting").is_none(), "KiCad's own name keeps regenerating in place");+    }++    #[test]+    fn board_shapes_in_kicads_terms() {+        let s = to_native("kicad_import_svg", "kicad_import_svg", &json!({"filePath": "b.kicad_pcb", "at": [120, -95], "widthMm": 10, "frame": "standard"})).unwrap().unwrap();+        assert_eq!((s.args["x"].as_f64(), s.args["y"].as_f64(), s.args["width"].as_f64()), (Some(120.0), Some(95.0), Some(10.0)), "{}", s.args);+        assert!(s.args.get("frame").is_none() && s.args.get("at").is_none());+        let s = to_native("kicad_copy_routing", "kicad_copy_routing", &json!({"box": [10, -25, 30, -10], "offset": [25, 5], "frame": "standard"})).unwrap().unwrap();+        assert_eq!(s.args["source"], json!({"minX": 10.0, "minY": 10.0, "maxX": 30.0, "maxY": 25.0}), "{}", s.args);+        assert_eq!(s.args["offset"], json!([25, -5.0]));+        let s = to_native("kicad_align_parts", "kicad_align_footprints", &json!({"refs": ["C1", "C2"], "mode": "top", "to": -40, "frame": "standard"})).unwrap().unwrap();+        assert_eq!(s.args["to"].as_f64(), Some(40.0));+        let s = to_native("kicad_edit_footprint", "kicad_edit_footprint", &json!({"libraryPath": "C:/lib/Mine.pretty", "name": "SOT-23", "pads": [{"number": "1", "set": {"sizeMm": [1, 0.8]}}]})).unwrap().unwrap();+        assert_eq!(s.command, "kicad_fp_edit", "a library edit under the standard's name goes to fp_edit");+        assert_eq!(s.args["pads"][0]["set"]["size"], json!([1, 0.8]));+        let s = to_native("kicad_edit_footprint", "kicad_edit_footprint", &json!({"refs": ["R7"], "value": "4k7"})).unwrap().unwrap();+        assert_eq!(s.command, "kicad_edit_footprint", "KiCad's own board edit is unchanged");+        let s = to_native("kicad_export_ipc2581", "kicad_export_ipc2581", &json!({"bomFields": {"mpn": "MPN", "distributor": "LCSC"}})).unwrap().unwrap();+        assert_eq!((s.args["bomColMfgPn"].as_str(), s.args["bomColDist"].as_str()), (Some("MPN"), Some("LCSC")));+        let s = to_native("kicad_sch_sheet_pins", "kicad_sch_sheet_pins", &json!({"sheet": "C:/p/main.kicad_sch", "sheetSymbol": "Power", "kind": "input"})).unwrap().unwrap();+        assert_eq!((s.args["filePath"].as_str(), s.args["sheet"].as_str(), s.args["shape"].as_str()), (Some("C:/p/main.kicad_sch"), Some("Power"), Some("input")));+        let e = to_native("kicad_sch_items", "kicad_sch_items", &json!({"frame": "upside"})).unwrap();+        assert!(e.is_err());+        assert!(to_native("kicad_route_net", "kicad_route_net", &json!({})).is_none(), "verbs outside the 47 are left alone");+    }+}+--- a/rust/crates/kicad-bridge/src/verbs.rs+++ b/rust/crates/kicad-bridge/src/verbs.rs@@ -1,606 +1,620 @@⋯ 11 unchanged lines ⋯ use kicad_platform::native; use crate::Caller; -pub fn dispatch(state: &mut State, command_in: &str, args: &Value, _caller: &Caller) -> Value {+pub fn dispatch(state: &mut State, command_in: &str, args: &Value, caller: &Caller) -> Value {     // ab forwards the bare name (it strips the `kicad_` prefix it routed on); the CLI and     // hand callers send the full one. Accept both, like the Python bridge.     let mut full: String = if command_in.starts_with("kicad_") { command_in.to_string() } else { format!("kicad_{command_in}") };     if full == "kicad_export_gerbers" {         full = "kicad_export_gerber".into(); // alias kept from the Python bridge     }+    let called = full.clone();     // the EDA Bridge Standard's name for a verb this bridge has under its own (catalog::ALIASES)     if let Some(t) = catalog::alias_target(&full) {         full = t.to_string();     }-    let command = full.as_str();+    // the standard's argument shapes for the verbs this bridge proposed to it (std_shapes.rs)+    match crate::std_shapes::to_native(&called, &full, args) {+        None => run(state, &full, args, caller),+        Some(Err(refusal)) => refusal,+        Some(Ok(shaped)) => {+            let mut out = run(state, &shaped.command, &shaped.args, caller);+            crate::std_shapes::from_native(&shaped, &mut out);+            out+        }+    }+}++fn run(state: &mut State, command: &str, args: &Value, _caller: &Caller) -> Value {     let Some(verb) = catalog::find(command) else {         let names: Vec<&str> = catalog::all().iter().map(|v| v.name).collect();         return fail("unknown_verb", format!("Unknown command: {command}"), format!("Valid commands: {}", names.join(", ")));⋯ 574 unchanged lines ⋯         _ => catalog.into(),     } }+--- a/rust/crates/kicad-bridge/src/verbs_fab.rs+++ b/rust/crates/kicad-bridge/src/verbs_fab.rs@@ -1,487 +1,501 @@⋯ 386 unchanged lines ⋯     if quantities.is_empty() {         return fail("bad_arg", "quantities must be positive integers", "e.g. [5, 10, 100]");     }+    if let Some(f) = args.get("fab").and_then(Value::as_str) {+        if !f.eq_ignore_ascii_case("jlcpcb") {+            return fail("not_supported", format!("No price list for {f}: the estimate knows JLCPCB-style rates only"), "Leave fab out (or pass jlcpcb); for another fab, send it the manufacturing files for a quote.");+        }+    }     let assembly = args.get("assembly").and_then(Value::as_bool).unwrap_or(numbers["totalParts"].as_u64().unwrap_or(0) > 0);     let table = fab::cost_table(&numbers, &quantities, assembly, args.get("basicParts").and_then(Value::as_u64));     let mut warnings = Vec::new();⋯ 5 unchanged lines ⋯         "output": format!("{} x {} mm, {} layer(s), {} SMD + {} THT parts ({} unique)", numbers["widthMm"], numbers["heightMm"], numbers["layers"], numbers["smdParts"], numbers["thtParts"], numbers["uniqueParts"]),         "path": norm(&board),         "board": numbers,+        "prices": table["rows"].as_array().map(|rows| rows.iter().map(|r| json!({+            "quantity": r["quantity"], "board": r["pcbUsd"], "assembly": r.get("assemblyUsd").cloned().unwrap_or(Value::Null),+            "total": r["totalUsdExcludingParts"], "currency": "USD"})).collect::<Vec<_>>()).unwrap_or_default(),+        "assumptions": std::iter::once(json!("Built-in JLCPCB-style rates, never checked against the fab's price page: not a current price."))+            .chain(table["assumptions"].as_array().cloned().unwrap_or_default()).collect::<Vec<_>>(),+        // the EDA Bridge Standard asks every outside number to name its source and the date it was read+        "source": {"name": "JLCPCB-style rates built into kicad-bridge (approximate, not a live price list)", "url": "https://jlcpcb.com/quote",+                   "read": Value::Null, "readNote": "The rates were written into the bridge by hand; the date they were last checked against jlcpcb.com is not recorded, so treat them as dated."},         "estimate": table,         "warnings": warnings,-        "_hint": "This is a rough estimate with the assumptions listed, not a quote. For a real one: kicad_export_fab_package {fab:\"jlcpcb\"}, then adom-instapcb-quote on the zip (it prices the BOM too). adom-parts-search prices single parts.",+        "_hint": "This is a rough estimate from built-in rates, never checked against the fab's price page, with the assumptions listed: not a quote and not a current price. For a real one: kicad_export_fab_package {fab:\"jlcpcb\"}, then adom-instapcb-quote on the zip (it prices the BOM too). adom-parts-search prices single parts.",     }) } ⋯ 78 unchanged lines ⋯         let _ = std::fs::remove_dir_all(&d);     } }+--- a/rust/crates/kicad-core/src/board_edit.rs+++ b/rust/crates/kicad-core/src/board_edit.rs@@ -1,1505 +1,1530 @@⋯ 949 unchanged lines ⋯ }  /// `kicad_edit_footprint` (file form): value, reference, fields, attributes and lock.-pub fn edit_footprints(text: &str, args: &Value) -> RResult<Change> {-    let root = parse_root(text)?;+/// The edit each footprint gets: `edits: [{ref, ...}]` gives every part its own (the EDA Bridge+/// Standard's edit_parts); otherwise `ref`/`refs` share the call's own arguments.+pub fn edits_by_ref(args: &Value) -> RResult<Vec<(String, Value)>> {+    if let Some(edits) = args.get("edits") {+        let list = edits.as_array().filter(|a| !a.is_empty()).ok_or_else(|| err("invalid_args", "edits is a list of {ref, ...the fields to change}"))?;+        let mut out: Vec<(String, Value)> = Vec::new();+        for (i, e) in list.iter().enumerate() {+            let r = e.get("ref").and_then(Value::as_str).map(str::trim).filter(|s| !s.is_empty()).ok_or_else(|| err("missing_ref", format!("edits[{i}] has no ref")))?;+            if out.iter().any(|(x, _)| x.eq_ignore_ascii_case(r)) {+                return Err(err("invalid_refs", format!("{r} is edited twice; merge its edits into one entry")));+            }+            out.push((r.to_string(), e.clone()));+        }+        if out.len() > 256 {+            return Err(err("invalid_refs", "At most 256 refs per call"));+        }+        return Ok(out);+    }     let refs = refs_arg(args)?;-    let fps = find_refs(&root, &refs)?;-    if fps.len() > 1 && args.get("newReference").or_else(|| args.get("reference")).is_some() {+    if refs.len() > 1 && args.get("newReference").or_else(|| args.get("reference")).is_some() {         return Err(err("invalid_args", "newReference renames one footprint; pass a single ref"));     }+    Ok(refs.into_iter().map(|r| (r, args.clone())).collect())+}++pub fn edit_footprints(text: &str, args: &Value) -> RResult<Change> {+    let root = parse_root(text)?;+    let edits = edits_by_ref(args)?;+    let refs: Vec<String> = edits.iter().map(|(r, _)| r.clone()).collect();+    let fps = find_refs(&root, &refs)?;     let taken = existing_refs(&root);     let mut rows = Vec::new();     let mut report = Vec::new();     for f in &fps {-        let (b, changed) = edit_one(f.src(text), args, &taken)?;+        let own = edits.iter().find(|(r, _)| r.eq_ignore_ascii_case(&fp_ref(f))).map(|(_, a)| a).unwrap_or(args);+        let (b, changed) = edit_one(f.src(text), own, &taken)?;         let nf = sxedit::parse(&b).map_err(|e| err("edit_failed", e))?;         report.push(json!({"ref": fp_ref(f), "changed": changed, "before": footprint_info(f), "after": footprint_info(&nf)}));         rows.push((*f, b));     }     let mut c = replace_blocks(text, &root, &rows, format!("Adom edit {}", refs.join(", ")), false, json!({"edited": report}))?;-    c.copper = args.get("attributes").is_some() || fp_text::ATTR_FLAGS.iter().any(|(k, _)| args.get(*k).is_some());+    c.copper = edits.iter().any(|(_, a)| a.get("attributes").is_some() || fp_text::ATTR_FLAGS.iter().any(|(k, _)| a.get(*k).is_some()));     Ok(c) } ⋯ 529 unchanged lines ⋯         assert_eq!(mounting_hole_lib_id(3.0, false), None);     } }+--- a/rust/crates/kicad-core/src/install.rs+++ b/rust/crates/kicad-core/src/install.rs@@ -1,2757 +1,2763 @@⋯ 1773 unchanged lines ⋯         "" => documents_dir(&user_dir).join(&proj),         d => PathBuf::from(d),     };+    // called as the standard's create_part_project: never overwrite without overwrite:true+    if args.get("_refuseExisting") == Some(&json!(true)) && out_dir.join(format!("{proj}.kicad_pro")).exists() {+        return json!({"success": false, "errorCode": "project_exists", "error": format!("A project named {proj} already exists in {}", norm_path(&out_dir)),+            "_hint": "Pass overwrite:true to regenerate it in place, or another projectName."});+    }     if let Err(e) = std::fs::create_dir_all(&out_dir) {         return json!({"success": false, "error": format!("Failed to write project files: cannot create {}: {e}", norm_path(&out_dir))});     }⋯ 975 unchanged lines ⋯         let _ = std::fs::remove_dir_all(&root);     } }+--- a/rust/crates/kicad-core/src/ipc/board_live.rs+++ b/rust/crates/kicad-core/src/ipc/board_live.rs@@ -1,719 +1,731 @@⋯ 415 unchanged lines ⋯ /// footprint proto (UpdateItems, the footprint keeps its UUID); field edits go through the /// delete-and-create path. pub fn edit_footprint(ctx: &Ctx, args: &Value) -> RResult<Value> {-    let proto_only = args.as_object().map(|o| o.keys().all(|k| PROTO_KEYS.contains(&k.as_str()) || !is_change_key(k))).unwrap_or(false)-        && args.get("attributes").and_then(Value::as_object).map(|a| !a.contains_key("type")).unwrap_or(true);+    let edits = board_edit::edits_by_ref(args)?;+    let proto_only = edits.iter().all(|(_, a)| {+        a.as_object().map(|o| o.keys().all(|k| PROTO_KEYS.contains(&k.as_str()) || !is_change_key(k))).unwrap_or(false)+            && a.get("attributes").and_then(Value::as_object).map(|x| !x.contains_key("type")).unwrap_or(true)+    });     if !proto_only {         return commit_change(ctx, args, |text| board_edit::edit_footprints(text, args));     }⋯ 17 unchanged lines ⋯         return Err(RoutingError::new("footprint_not_found", "The editor no longer holds every footprint; read the board again").with("mutated", json!(false)));     }     for it in items.iter_mut() {-        edit_footprint_proto(it, args)?;+        // each footprint takes its own edit (edits: [{ref, ...}]), else the call's arguments+        let own = match it {+            EditablePcbItem::Footprint(fp) => {+                let r = fp.proto().reference_field.as_ref().and_then(|f| f.text.as_ref()).and_then(|t| t.text.as_ref()).map(|t| t.text.clone()).unwrap_or_default();+                edits.iter().find(|(e, _)| e.eq_ignore_ascii_case(&r)).map(|(_, a)| a).unwrap_or(args)+            }+            _ => args,+        };+        edit_footprint_proto(it, own)?;     }     let n_items = items.len();     transaction(&s, &change.label, || {⋯ 269 unchanged lines ⋯         assert_eq!(edit_footprint_proto(&mut item, &json!({"newReference": "R9"})).unwrap_err().code, "ipc_error");     } }+--- a/skills/kicad-bridge-dev/SKILL.md+++ b/skills/kicad-bridge-dev/SKILL.md@@ -1,1039 +1,1043 @@⋯ 142 unchanged lines ⋯ - **The standard has the job**: name the verb after it (`kicad_net_classes`, not `kicad_netclasses`), or keep your   name and add `(standard name, your verb)` to `catalog::ALIASES`, so both names are served. - **It covers part of the job**: keep your name, list the verb in `kicad_only.txt`, and record it as partial in the-  standard's `tools/equivalents.json`.-- **The standard has no such job**: list the verb in `rust/crates/kicad-bridge/src/kicad_only.txt`.+  standard's `tools/equivalents/kicad.json`.+- **The standard has no such job**: list the verb in `rust/crates/kicad-bridge/src/kicad_only.txt`. If other EDA+  tools could do the job, propose it to the standard (an issue titled `kicad-lane: propose ...` on its page; once+  the standard-lane thread agrees, the entry goes in `tools/proposed/kicad-lane.json`). When it lands, take the+  verb out of `kicad_only.txt` and give it the standard's arguments in `std_shapes.rs`.  `catalog.rs`'s test `every_verb_has_a_standard_decision` fails until every verb is one of the three, and `bridge_json_declares_exactly_the_catalog` fails until `bridge.json` lists it. Each new verb also needs an entry in⋯ 886 unchanged lines ⋯ - **A headless VM hands new windows the foreground by itself.** With no console user, every   background launch measures at z=0; that is the environment, not a focus steal (see the test   skill's victim rule).+--- a/skills/kicad-bridge-publish/SKILL.md+++ b/skills/kicad-bridge-publish/SKILL.md@@ -1,283 +1,285 @@⋯ 278 unchanged lines ⋯ page's pinned issue #3) or open an issue titled `kicad-lane: <what>`. Never run the standard's `generate.py` and push its outputs; the release script prints the coverage it expects instead. A verb that does a standard job under another name gets an alias in `catalog::ALIASES` first (or an entry in the kicad section of the standard's-`tools/equivalents.json`), or the coverage table does not count it. A job the standard lacks is proposed on an issue+`tools/equivalents/kicad.json`), or the coverage table does not count it. A job the standard lacks is proposed on an issue there, then added to `tools/proposed.json` once standard-lane agrees.++--- a/skills/kicad-bridge-test/run_verb_tests.py+++ b/skills/kicad-bridge-test/run_verb_tests.py@@ -1,1364 +1,1453 @@⋯ 1292 unchanged lines ⋯         r = F("import_svg", {"svg": svg, "x": round(cx, 2), "y": round(cy, 2), "width": 3, "layer": "Dwgs.User"}); rec("import_svg", r, ok(r))         r = F("delete_graphics", {"layer": "Dwgs.User"}); rec("delete_graphics", r, ok(r)) +    def std_phase():+        """Phase 9: the EDA Bridge Standard's shapes for the 47 verbs KiCad proposed to it (standard 0.3.0):+        the standard names, sheet/project paths, id, lists as one write, frame:"standard" (y up) read back+        against KiCad's own frame. Recorded as std_<verb>; the verbs themselves are covered by phase 8."""+        if a.skip_gui or (only and not any(o.startswith("std") for o in only)):+            return+        WHY = "daily verb test: EDA Bridge Standard shapes"+        S = lambda verb, args, t=200: ab(a.target, f"kicad_{verb}", dict(args, reason=WHY), timeout=t)+        def rec(name, r, good, why=None):+            record("std_" + name, r, bool(good), None if good else (why or f"{r.get('errorCode')}: {str(r.get('error'))[:160]}"))+        clean = lambda r: bool(r.get("errorCode")) and r.get("mutated") is not True+        tag = datetime.now().strftime("%m%d%H%M%S")+        base, OUT = w["out"].rsplit("/", 1)[0], w["out"]+        P0 = base + "/std-" + tag+        pd = P0 + "/kstd"+        pro, sch = pd + "/kstd.kicad_pro", pd + "/kstd.kicad_sch"++        print("[9.0] describe marks the standard shapes")+        d = ab(a.target, "kicad_describe", {}, timeout=90)+        marked = {v["name"] for v in (d.get("verbs") or []) if isinstance(v, dict) and v.get("standardShape")}+        rec("describe", {"marked": len(marked)}, "kicad_sch_text" in marked and "kicad_edit_parts" in marked and len(marked) >= 47, f"{len(marked)} verbs marked")++        print("[9.1] a sheet in the standard's shapes")+        r = S("project_create", {"parentDir": P0, "name": "kstd"}); rec("project_create", r, ok(r))+        r = S("project_info", {"project": pro}); rec("project_info", r, ok(r))+        r = S("sch_page", {"sheet": sch, "paper": "A4", "orientation": "landscape", "title": "kstd"}); rec("sch_page", r, ok(r))+        for ref, x in (("R1", 50.8), ("R2", 76.2)):+            ab(a.target, "kicad_sch_place_symbol", {"filePath": sch, "libId": "Device:R", "reference": ref, "at": [x, 50.8]}, timeout=150)+        k = S("sch_pins", {"sheet": sch, "references": ["R1"]})+        u = S("sch_pins", {"sheet": sch, "references": ["R1"], "frame": "standard"})+        def pin_at(rr, num):+            return next((p.get("at") for p in (rr.get("pins") or []) if str(p.get("number")) == num), None)+        ka, ua = pin_at(k, "2"), pin_at(u, "2")+        rec("sch_pins_frame", {"kicad": ka, "standard": ua}, ka and ua and abs(ua[0] - ka[0]) < 1e-6 and abs(ua[1] + ka[1]) < 1e-6 and u.get("frame") == "standard",+            f"R1.2 at {ka} in KiCad's frame but {ua} in the standard's (y should be negated)")+        r = S("sch_connect_pins", {"sheet": sch, "pairs": [{"from": "R1.2", "to": "R2.2"}, {"from": "R1.1", "to": "R2.1", "first": "v"}]})+        rec("sch_connect_pins", r, ok(r) and r.get("batchedAs") == "kicad_sch_batch" and len(r.get("routes") or []) >= 2)+        r = S("sch_trace_point", {"sheet": sch, "at": ua, "toleranceMm": 0.1, "frame": "standard"}) if ua else {"error": "no pin"}+        rec("sch_trace_point", r, ok(r) and "R1.2" in json.dumps(r) and "R2.2" in json.dumps(r), "the y-up point did not find R1.2 joined to R2.2")+        r = S("sch_text", {"sheet": sch, "frame": "standard", "texts": [{"text": "std up", "at": [30.48, -30.48], "sizeMm": 2}, {"text": "std two", "at": [30.48, -35.56]}]})+        rec("sch_text", r, ok(r) and r.get("batchedAs") == "kicad_sch_batch")+        r = S("sch_items", {"sheet": sch, "kinds": ["texts"], "frame": "standard"})+        t = next((x for x in (r.get("texts") or []) if x.get("text") == "std up"), {})+        rec("sch_items", r, ok(r) and t.get("id") and t.get("at") == [30.48, -30.48], f"text read back as {t}")+        r = S("sch_junction", {"sheet": sch, "points": [[30.48, 60.96]]})+        jid = next((j.get("id") for j in (S("sch_items", {"sheet": sch, "kinds": ["junctions"]}).get("junctions") or []) if j.get("at") == [30.48, 60.96]), None)+        r = S("sch_junction", {"sheet": sch, "points": [[35.56, 60.96]], "remove": [jid] if jid else []}) if jid else {"error": "the added junction has no id"}+        rec("sch_junction", r, ok(r) and r.get("batchedAs") == "kicad_sch_batch")+        ab(a.target, "kicad_sch_connect_to_net", {"filePath": sch, "pin": "R1.1", "net": "VIN"}, timeout=120)+        r = S("sch_edit_label", {"sheet": sch, "frame": "standard", "edits": [{"net": "VIN", "by": [0, 2.54]}]}); rec("sch_edit_label", r, ok(r))+        r = S("sch_create", {"sheet": pd + "/std-extra.kicad_sch", "orientation": "portrait", "title": "extra"}); rec("sch_create", r, ok(r))+        r = S("sch_hierarchy", {"project": pro}); rec("sch_hierarchy", r, ok(r))+        r = S("render_schematic", {"sheet": sch, "outputDir": OUT + "/std-render-sch"}, 220); rec("render_schematic", r, ok(r))+        r = S("project_snapshot", {"project": pro, "label": "std", "pdfs": False}, 220); rec("project_snapshot", r, ok(r))+        r = S("project_rename", {"project": pro, "newName": "kstd2", "dryRun": True}); rec("project_rename", r, ok(r) and r.get("mutated") is not True)++        print("[9.2] board, library and outputs in the standard's shapes")+        B, F = w["board"], w["offline"]+        st = ab(a.target, "kicad_routing_state", {"filePath": F, "model": "standard"}, timeout=150)+        refs = [p.get("ref") for p in (st.get("components") or st.get("parts") or []) if str(p.get("ref", "")).startswith(("C", "R"))][:2]+        if len(refs) == 2:+            r = S("edit_parts", {"filePath": F, "source": "file", "edits": [{"ref": refs[0], "value": "2u2"}, {"ref": refs[1], "value": "4k7", "dnp": True}]})+            rec("edit_parts", r, ok(r) and len(r.get("edited") or []) == 2)+            r = S("align_parts", {"filePath": F, "refs": refs, "mode": "top", "dryRun": True, "allowOverlap": True, "frame": "standard"})+            rec("align_parts", r, ok(r) and r.get("inputFrame") == "standard")+            r = S("remove_parts", {"filePath": F, "refs": refs[:1], "dryRun": True, "source": "file"}); rec("remove_parts", r, ok(r) and r.get("mutated") is not True)+        else:+            rec("edit_parts", st, False, "no two C/R parts on the offline board")+        svg = "<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect width='10' height='10'/></svg>"+        r = S("import_svg", {"filePath": F, "svg": svg, "at": [100, -100], "widthMm": 3, "layer": "Dwgs.User", "frame": "standard", "dryRun": True, "source": "file"})+        rec("import_svg", r, ok(r) and r.get("inputFrame") == "standard")+        r = S("remove_graphics", {"filePath": F, "layer": "Dwgs.User", "dryRun": True, "source": "file"}); rec("remove_graphics", r, ok(r) and r.get("mutated") is not True)+        r = S("check_clearance", {"filePath": B, "mode": "pads", "thresholdMm": 0.2}); rec("check_clearance", r, ok(r))+        r = S("cross_probe", {"filePath": B, "ref": "U1"}); rec("cross_probe", r, ok(r) or clean(r))+        r = S("estimate_cost", {"filePath": B, "quantities": [5, 50]})+        rec("estimate_cost", r, ok(r) and (r.get("source") or {}).get("name") and "read" in (r.get("source") or {}) and len(r.get("prices") or []) == 2)+        r = S("export_ipc2581", {"filePath": B, "outputDir": OUT + "/std-ipc2581", "bomFields": {"mpn": "MPN"}}, 300); rec("export_ipc2581", r, ok(r))+        r = S("export_dxf", {"filePath": B, "outputDir": OUT + "/std-dxf", "layers": ["Edge.Cuts"], "contours": True}, 300); rec("export_dxf", r, ok(r))+        r = S("render_board_3d", {"filePath": B, "side": "top", "outputDir": OUT + "/std-render3d"}, 600); rec("render_board_3d", r, ok(r) and r.get("image"))+        r = S("read_footprint", {"libId": "Resistor_SMD:R_0603_1608Metric"}); rec("read_footprint", r, ok(r))+        pretty = f"{OUT}/KSTD{tag}.pretty"+        S("create_footprint", {"libraryPath": pretty, "name": "KSTD_FP", "pads": [{"number": 1, "size": [1, 1], "at": [-1, 0]}, {"number": 2, "size": [1, 1], "at": [1, 0]}]})+        r = S("edit_footprint", {"libraryPath": pretty, "name": "KSTD_FP", "pads": [{"number": "1", "set": {"sizeMm": [1.2, 1]}}]})+        rec("edit_footprint_library", r, ok(r), "the standard's edit_footprint with library arguments did not reach kicad_fp_edit")+        r = S("export_library_part", {"part": "__no_such_part__"}); rec("export_library_part", r, r.get("success") is False)+        r = S("create_part_project", {"part": "__no_such_part__"}); rec("create_part_project", r, r.get("success") is False)+     live_phase()     k161_phase()+    std_phase()      # phase 7: clean up EVERYTHING the suite created on the box (John 2026-08-14)     if not only and not a.skip_gui:⋯ 61 unchanged lines ⋯  if __name__ == "__main__":     sys.exit(main())+--- a/skills/kicad-schematic-and-board-edits/SKILL.md+++ b/skills/kicad-schematic-and-board-edits/SKILL.md@@ -1,58 +1,82 @@⋯ 54 unchanged lines ⋯ `export_ipc2581`, `export_ipc356`, `export_gencad`, `export_dxf`, `validate_for_fab` (the standard's `fab_check`), `estimate_cost`. +## The EDA Bridge Standard's shapes (from 1.0.63)++These verbs are in the [EDA Bridge Standard](https://wiki.adom.inc/adom/eda-bridge-standard) (0.3.0), so an AI+that knows the standard can call them under the standard's names and arguments; KiCad's own names and arguments+keep working. `kicad_describe` marks each one with `standardShape`.++- **Names**: `kicad_render_board_3d` (render_board), `kicad_remove_parts` (delete_footprint), `kicad_edit_parts`+  (edit_footprint on the board), `kicad_align_parts`, `kicad_remove_graphics`, `kicad_read_footprint` (fp_info),+  `kicad_export_library_part` (export_part), `kicad_create_part_project` (make_part_project, which then refuses+  to overwrite an existing project unless `overwrite:true`). `kicad_edit_footprint` with library arguments+  (`libraryPath` + `name`, no `ref`) edits the LIBRARY footprint (fp_edit), as the standard means it.+- **Arguments**: `sheet` and `project` paths, `id`/`ids` for uuids, mm-named numbers (`toleranceMm`, `gapMm`,+  `thresholdMm`, `widthMm`...), `bomFields` on IPC-2581, `at` and `box` on import_svg and copy_routing.+- **Lists are one write**: `sch_connect_pins {pairs: [{from, to}]}`, `sch_bus_entry {entries}`,+  `sch_edit_label {edits}`, `sch_text {texts}` and `sch_junction {points, remove: [id or [x, y]]}` run as one+  `kicad_sch_batch` (`batchedAs` says so). `kicad_edit_parts {edits: [{ref, value, dnp...}]}` gives each part its+  own edit in one undo step.+- **y up**: `frame:"standard"` takes coordinates with KiCad's y negated. A sheet verb's reply comes back y up+  too (`frame:"standard"` in it); a board verb's reply stays in KiCad's frame (`inputFrame`), and+  `kicad_routing_state {model:"standard"}` reads the board back y up.+- `estimate_cost` names its price `source` (built-in JLCPCB-style rates, with no recorded check date) and lists+  `prices` per quantity.+ Every verb here runs in the verb battery on a real Windows box before each release.+--- a/tools/build_native_release.py+++ b/tools/build_native_release.py@@ -1,291 +1,307 @@⋯ 271 unchanged lines ⋯         return     ref = f"reference/kicad-{ver}.json"     (fresh / ref).write_text(json.dumps({"bridge": "kicad", "version": ver, "verbs": verbs}, indent=1) + "\n")-    r = subprocess.run(["adom-wiki", "repo", "push", "adom/eda-bridge-standard", "-m", f"kicad-lane: KiCad bridge {ver} reference", "--files", ref],+    files = [ref]+    # An exact equivalent the release now answers by the standard's own name (an alias in catalog::ALIASES pointing+    # at the same verb) is dropped in the same push, so no regeneration ever sees neither credit it. A partial one+    # stays: the equivalents entry wins over the name match and keeps the credit honest.+    eqf = fresh / "tools/equivalents/kicad.json"+    if eqf.exists():+        aliases = dict(re.findall(r'\("kicad_(\w+)", "kicad_(\w+)"\)', (RUST / "crates/kicad-bridge/src/catalog.rs").read_text()))+        eq = json.loads(eqf.read_text())+        named = [k for k, (v, m) in eq.items() if m == "exact" and aliases.get(k) == v.strip() and f"kicad_{k}" in verbs]+        if named:+            for k in named:+                eq.pop(k)+            eqf.write_text(json.dumps(eq, indent=1) + "\n")+            files.append("tools/equivalents/kicad.json")+            print(f"EDA Bridge Standard: dropping equivalents now answered by name: {', '.join(named)}")+    r = subprocess.run(["adom-wiki", "repo", "push", "adom/eda-bridge-standard", "-m", f"kicad-lane: KiCad bridge {ver} reference", "--files"] + files,                        cwd=fresh, capture_output=True, text=True)     ok = '"ok": true' in r.stdout or "PUSH_UNCHANGED" in r.stdout + r.stderr     if not ok:⋯ 3 unchanged lines ⋯     subprocess.run(["adom-wiki", "repo", "clone", "adom/eda-bridge-standard", "--dir", str(check)], capture_output=True, text=True)     same = (check / ref).exists() and (check / ref).read_text() == (fresh / ref).read_text()     g = subprocess.run([sys.executable, "tools/generate.py"], cwd=check, capture_output=True, text=True)-    print(f"EDA Bridge Standard: pushed {ref} ({'read back' if same else 'NOT found on a fresh clone'}); "+    print(f"EDA Bridge Standard: pushed {', '.join(files)} ({'read back' if same else 'NOT found on a fresh clone'}); "           f"expected after regeneration: {g.stdout.strip() or g.stderr[-200:]}. Now ping standard-lane to regenerate: "           f"a SendMessage to the session named in adom/eda-bridge-standard issue #3, or a 'kicad-lane:' issue there.")   if __name__ == "__main__":     main()+

Comments

No comments yet.

Log in to comment.