← Commit history

v0.1.5: Barrett consolidation — final parity verbs + the part-import and kicad-import skills in the master skillpack

John Lauer ·77f8b99889 ·1mo ago ·parent 9178440
11 files changed +523−13
CHANGELOG.md+5
@@ -1,5 +1,10 @@ # Changelog +## 0.1.5 — 2026-08-30+- Barrett consolidation complete: cleanup_libs, set_visibility, xcopy_footprint (cross-library), and import_part (his fat verb on the resident lane) — full verb parity with barrett-land/adom-desktop-altium-bridge.+- Skillpack: the pkg now ships altium-part-import (his 3-path catalog-part workflow, rewritten on these verbs, with the tri-vendor rule: Mouser preferred + DigiKey + LCSC part numbers on every component) and altium-kicad-import (his measured KiCad-10 findings + the vendored v10→v9 downgrader + the native no-importer lane).+- Tools: kicad_v10_to_v9.py vendored from barrett-land/kicad10-to-altium.+ ## 0.1.4 — 2026-08-30 - Barrett-parity library manipulation: clone_symbol (all display modes, 555 primitives verified), rename_symbol, read_symbol (pins/params/modes read-back), copy_footprint (pads+bodies+drawing), rename_footprint, remove_footprint. Smoke-tested on the LAN7800 libraries with zero dialogs. 
README.md+2−2
@@ -106,7 +106,7 @@ A halted engine (DelphiScript runtime errors are modal) recovers with `altium_ag  ## Verb reference -All 77 verbs (prefix `altium_`). Call `altium_describe` for the live catalog.+All 81 verbs (prefix `altium_`). Call `altium_describe` for the live catalog.  **Readiness & lifecycle** — `readiness`, `status`, `launch`, `close`, `diagnostics`, `progress`, `agent_start/_stop/_status/_recover`, `license`, `signin_status`, `connect_workspace`, `uninstall`@@ -127,7 +127,7 @@ layers), `build_symbol_modes` (one symbol, N display modes) + `symbol_mode` (swi  **Manipulate libraries** — `clone_symbol` (all display modes carried), `rename_symbol`, `remove_symbol`, `read_symbol` (pins/params/modes read-back), `copy_footprint` (pads + bodies +-drawing), `rename_footprint`, `remove_footprint`, `normalize_bodies`+drawing), `rename_footprint`, `remove_footprint`, `xcopy_footprint` (cross-library), `cleanup_libs`, `set_visibility`, `normalize_bodies`, and `import_part` — one call: footprint spec + STEP + symbol + vendor params (Mouser preferred, DigiKey, LCSC)  **Boards & placement** — `place_on_sch`, `place_on_pcb`, `test_board` (fresh PcbDoc, board shape sized to the part, nets + stub tracks), `board_shape`, `swap_footprint` (change the package on a
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.4",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.4/adom-bridge-altium-v0.1.4.zip",-  "sha256": "18d981e360c0cef06edb7cd1292267909c6fe52e9da7dfebc4a04d2156e92054",-  "size": 91365,+  "version": "0.1.5",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.5/adom-bridge-altium-v0.1.5.zip",+  "sha256": "a187c33c158e97660e9c481523813fa3299fa7a4d6740693b107c81b8f1020e5",+  "size": 92792,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.5.zipadded
⋯ 1 unchanged line ⋯
bridge.json+17−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.4",+  "version": "0.1.5",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",@@ -152,6 +152,22 @@     {       "name": "altium_remove_footprint",       "description": "Remove a footprint from a PcbLib"+    },+    {+      "name": "altium_cleanup_libs",+      "description": "Remove factory-default stray components from the current libraries"+    },+    {+      "name": "altium_set_visibility",+      "description": "Show/hide a named parameter on a SchLib component"+    },+    {+      "name": "altium_xcopy_footprint",+      "description": "Copy a footprint across libraries (pads+bodies+drawing)"+    },+    {+      "name": "altium_import_part",+      "description": "One-call part import: footprint spec + STEP + symbol spec + models + vendor params"     }   ],   "statusVerb": "altium_status",
install.sh+4−2
@@ -7,8 +7,10 @@ install_skill() {  # <dest-dir> <src-file>   mkdir -p "$1" && cp -f "$2" "$1/SKILL.md" } for HOME_DIR in "$HOME/.claude/skills" "$HOME/.codex/skills"; do-  install_skill "$HOME_DIR/altium-bridge"       "$HERE/SKILL.md"-  install_skill "$HOME_DIR/altium-bridge-plan"  "$HERE/skills/altium-bridge-plan/SKILL.md"+  install_skill "$HOME_DIR/altium-bridge"        "$HERE/SKILL.md"+  install_skill "$HOME_DIR/altium-bridge-plan"   "$HERE/skills/altium-bridge-plan/SKILL.md"+  install_skill "$HOME_DIR/altium-part-import"   "$HERE/skills/altium-part-import/SKILL.md"+  install_skill "$HOME_DIR/altium-kicad-import"  "$HERE/skills/altium-kicad-import/SKILL.md" done  # Dashboard: the pkg ships it, the dock runs `altium-dashboard serve --print-url`.
package.json+14−3
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.1.4",+  "version": "0.1.5",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [@@ -32,7 +32,9 @@     "dashboard/ui/index.html",     "dashboard/ui/newbie-panel.js",     "dashboard/ui/altium-logo.png",-    "dashboard/ui/dock-altium.svg"+    "dashboard/ui/dock-altium.svg",+    "skills/altium-part-import/SKILL.md",+    "skills/altium-kicad-import/SKILL.md"   ],   "discovery_triggers": [     "open my design in altium",@@ -72,7 +74,16 @@     "altium screenshot",     "what is open in altium",     "altium dialog",-    "altium not responding"+    "altium not responding",+    "add a part to my altium library",+    "import a component into altium",+    "altium part with lcsc number",+    "altium part with mouser number",+    "add a resistor to altium",+    "import kicad symbol into altium",+    "kicad to altium",+    "convert kicad library to altium",+    "altium intlib import"   ],   "discovery_pitch": "Drive the user's real Altium Designer from an AI: open any design surface, author native libraries with 3D bodies, run DRC and OutJobs, and screenshot everything back.",   "sample_prompts": [
skills/altium-kicad-import/SKILL.mdadded+62
@@ -0,0 +1,62 @@+---+name: altium-kicad-import+description: >-+  Import KiCad parts into Altium Designer correctly, especially KiCad 10. Altium 26's own+  importer supports KiCad through 9 only — a raw v10 import silently drops property+  visibility (hidden fields arrive visible, piled on the symbol) and imports text in the+  wrong font. Two lanes: downgrade v10→v9 with tools/kicad_v10_to_v9.py then use Altium's+  importer, or convert natively with tools/kicad2altium.py + altium_build_footprint /+  altium_build_symbol_modes (no importer at all). Carries barrett-land/kicad10-to-altium's+  measured findings into the master altium-bridge skillpack. Trigger words — kicad to altium,+  kicad 10 altium import, overlapping fields after import, hidden fields visible, imported+  symbol wrong font, downgrade kicad 10 library.+---++# altium-kicad-import++Altium 26's KiCad importer understands the format **through KiCad 9**. KiCad 10 files import+but silently degrade — measured (Barrett Land, on AD 26.8/26.9, same `Device:R` from each+KiCad):++| Source | Importer warnings | Hidden fields preserved |+|---|---|---|+| KiCad 8 | 1 | 5 ✓ |+| KiCad 9 | 2 | 5 ✓ |+| **KiCad 10** | **7** | **0 ✗** |+| KiCad 10 → 9 (downgraded) | 2 | 5 ✓ |++Pins and body geometry survive in every case; it is the metadata that degrades. Also+measured: KiCad 9 cannot open a v10 library at all (`Unable to load library`) — there is no+"re-save it in KiCad 9" escape hatch, which is why the downgrader exists. On a real MCU+(STM32F103C8Tx, derived symbol) the downgrade ALSO needs symbols reordered base-first; the+tool does both. The one honest loss on MCUs either way: **alternate pin functions** (Altium+has no equivalent; pin names survive).++## Lane 1 — downgrade, then Altium's importer++```bash+python3 tools/kicad_v10_to_v9.py input_v10.kicad_sym output_v9.kicad_sym   # container side+# send output to the box, then drive File → Import Wizard (or the AdomBridge extension where it loads)+```+After import, normalise text to Altium's defaults (imported text arrives in KiCad's font):+every parameter/designator/comment should be `FontID=1`, colour `8388608`.++## Lane 2 — native conversion (no importer, no extension)++The bridge builds the part natively from the KiCad sources — this is how the CH340G and+LAN7800 on the adom/altium-bridge page were made:++```bash+python3 tools/kicad2altium.py part.kicad_mod fp.spec "<Name>" "<desc>"      # footprint spec+python3 tools/symbol_modes.py part.kicad_sym sym.spec <LIBREF> "<desc>"     # symbol (N display modes)+altium_build_footprint '{"libraryPath":"...PcbLib","spec":"C:/.../fp.spec"}'+altium_build_symbol_modes '{"libraryPath":"...SchLib","spec":"C:/.../sym.spec"}'+altium_add_body '{"libraryPath":"...PcbLib","footprintName":"...","step":"C:/.../part.step"}'+```+Native conversion sidesteps the importer entirely: no font damage, no visibility loss,+mechanical layers mapped (F.SilkS→Top Overlay, F.Fab→Mech13, F.CrtYd→Mech15, User.9→Mech9+"Adom.SolderBalls"), KiCad Y-down flipped, roundrect ratios carried, through-hole via pads by+duplicate designator.++Pick lane 2 when you control the flow end to end; lane 1 when the user hands you a .kicad_sym+and expects Altium's importer semantics.
skills/altium-part-import/SKILL.mdadded+105
@@ -0,0 +1,105 @@+---+name: altium-part-import+description: >-+  Add any catalog component (resistor, capacitor, inductor, IC, connector, diode…) to the+  user's Altium Designer library with Mouser, DigiKey AND LCSC/JLCPCB part numbers attached —+  Mouser is Adom's preferred vendor; the exact `LCSC Part #` parameter keeps JLCPCB BOM ++  pick-and-place round-tripping. Drives Altium through the adom/altium-bridge verbs (resident+  DelphiScript agent). Three paths, cheapest first: REUSE an existing symbol for a new value+  (clone + parametric footprint, no fetch), CREATE from a downloaded .IntLib or a wiki part+  page, APPEND into an existing library (cross-lib copy). Supersedes+  barrett-land/altium-part-importer (same workflow, this bridge's verbs). Use when the user+  wants to "add a part/component/resistor/IC/chip to Altium", "import a footprint or symbol+  into Altium", or build out an Altium component library. For Adom's OWN machine pins/contacts+  use adom-library-importer instead.+---++# altium-part-import++Adds catalog components to the user's **Altium Designer** libraries with the full vendor+identity attached. Every part gets a symbol, a footprint (with a 3D body), the+symbol→footprint link, and the parameter set below. Everything runs through the+`adom/altium-bridge` verbs (`adom-bridge --ai-thread "<thread>" altium_*`).++> Lineage: this is Barrett Land's altium-part-importer workflow, carried into the master+> altium-bridge skillpack and rewritten against the resident-agent verbs (his bridge ran one+> X2.EXE forward per call; this one holds a resident DelphiScript loop, so calls are cheap).++## The vendor rule (John, 2026-08-30)++**Mouser is Adom's preferred vendor.** Attach ALL THREE vendors to every part, not just LCSC:++| Parameter (exact name) | Example | Why |+|---|---|---|+| `Mouser Part #` | `579-LAN7800/Y9X` | preferred vendor — fastest, lowest-cost fulfilment |+| `DigiKey Part #` | `LAN7800/Y9X-ND` | the alternative when Mouser can't ship |+| `LCSC Part #` | `C633489` | exact name round-trips into the JLCPCB BOM/CPL |+| `MPN` + `Manufacturer` | `LAN7800`, `Microchip Technology` | identity |+| `Value`, `Package` | `100nF`, `0402` | the usual |++Look the numbers up with ONE `adom-parts-search search "<MPN>"` call (never loop searches —+shared daily quota). Description is human text **with the MPN in it**. On the schematic only+`Value` stays visible: parameters land hidden by default (`altium_set_params`), and+`altium_set_visibility {symbolName, param:"Value", visible:true}` shows the one you want.++## FIRST: survey — never blind-create++```bash+adom-bridge --ai-thread "<t>" altium_list_library '{"libraryPath":"<existing .SchLib>"}'+adom-bridge --ai-thread "<t>" altium_read_symbol '{"libraryPath":"...","symbolName":"..."}'+```+- Libraries exist → append into the right one, or create a new one alongside.+- None found → **ASK the user** where their libraries live before creating anything.+- House default location: `%USERPROFILE%\Documents\Altium\Libraries\` (or the project dir).++## Path A — REUSE (no fetch): a new VALUE of a part type you already have++```bash+# clone the symbol (all display modes carried), rename to the new value+altium_clone_symbol  '{"libraryPath":"<lib>.SchLib","from":"R_10k_0603_1%","to":"R_1k_0402_1%"}'+# parametric footprint: a spec is 2 pads for an 0402 (tools/kicad2altium.py emits these, or write by hand)+altium_build_footprint '{"libraryPath":"<lib>.PcbLib","spec":"C:/.../r0402.spec"}'+altium_add_body      '{"libraryPath":"<lib>.PcbLib","footprintName":"R0402","bwMil":39,"bhMil":20,"hMil":16}'+# vendor identity + footprint link+altium_add_models    '{"libraryPath":"<lib>.SchLib","symbolName":"R_1k_0402_1%","models":[{"name":"R0402","pcblib":"<lib>.PcbLib","description":"0402 chip resistor"}],"params":{"Mouser Part #":"...","DigiKey Part #":"...","LCSC Part #":"...","MPN":"...","Manufacturer":"...","Value":"1k","Package":"0402"},"comment":"1k","description":"1k 1% 0.1W 0402 (RC0402FR-071KL)"}'+altium_set_visibility '{"libraryPath":"<lib>.SchLib","symbolName":"R_1k_0402_1%","param":"Value","visible":true}'+```++## Path B — CREATE from a real catalog part++From a downloaded **.IntLib** (SnapEDA/UltraLibrarian/manufacturer):+```bash+altium_extract_intlib '{"intlibPath":"C:/Users/<u>/Downloads/CL05B104KO5NNNC.IntLib"}'  # -> .SchLib/.PcbLib sources+altium_rename_symbol / altium_rename_footprint                                          # house names+altium_add_models + altium_set_params                                                   # vendors + link+altium_install_library '{"libraryPath":"<lib>"}'+```+From a **wiki part page** (kicad_sym/kicad_mod/step files), the one-call lane:+```bash+python3 tools/kicad2altium.py part.kicad_mod fp.spec "<Footprint>" "<desc>"   # container side+altium_import_part '{"schLib":"...","pcbLib":"...","footprintSpec":"C:/.../fp.spec","step":"C:/.../part.step","params":{...},"description":"..."}'+```++## Path C — APPEND into an existing library++```bash+altium_xcopy_footprint '{"toLib":"<dest>.PcbLib","fromLib":"<src>.PcbLib","from":"TSSOP20","to":"TSSOP20"}'+altium_clone_symbol / altium_add_models   # as in Path A+```+Name collisions: if the destination already has that footprint name, prefer Path A (reuse the+existing footprint) rather than duplicating (`_1` suffixes break the symbol link).++## Gotchas (hard-won; Barrett's, still true, plus this bridge's)++- **The agent is serial.** One verb at a time. A timeout is NOT failure — the work usually+  persisted; verify by read-back (`altium_read_symbol`, `altium_pcb_report`) instead of retrying.+- **Survey + ask before creating a library.** No blind creation, ever.+- Build args with real JSON (`json.dumps`), never shell interpolation — Windows backslashes+  mangle into "invalid escape".+- Symbol and footprint libraries must be installed together (the model link resolves by+  footprint name across installed PcbLibs) — `altium_install_library` both.+- Fresh libraries carry factory strays: `altium_cleanup_libs` removes `Component_1` /+  `PCBComponent_1`.+- Every save is pre-cleared of dangling transactions and every dialog class we have met is+  pre-empted in the bridge — if you still see a dialog, that is a bug: capture it with+  `altium_check_dialogs` and report it.
tools/kicad_v10_to_v9.pyadded+309
@@ -0,0 +1,309 @@+#!/usr/bin/env python3+"""Downgrade a KiCad 10 .kicad_sym symbol library to KiCad 9 format.++WHY THIS EXISTS+KiCad 9 cannot open KiCad 10 libraries at all ("Unable to load library"), and Altium's+importer understands KiCad through 9 but degrades on 10 -- most importantly it drops property+VISIBILITY, so fields hidden in KiCad arrive visible in Altium. For a third-party library that+ships v10 only, there is no "just re-save in KiCad 9" escape hatch. Hence this.++WHAT ACTUALLY CHANGES (measured by diffing the same symbol out of KiCad 8/9/10):++  hide          RELOCATED, not renamed. v9 nests (hide yes) INSIDE (effects ...);+                v10 moved it OUT to be a direct child of (property ...). A regex that+                rewrites the token in place produces a file that is NEITHER version --+                KiCad 9 rejects it and Altium silently ignores the orphan token. This is+                the whole reason a real parser is needed.+  show_name             v10 only -> dropped (cosmetic: show the field name beside its value)+  do_not_autoplace      v10 only -> dropped (cosmetic: pin a field against auto-placement)+  in_pos_files          v10 only -> dropped. NOT a real loss: v9 expresses this on the+                        FOOTPRINT as (attr exclude_from_pos_files), which is what pcbnew+                        actually reads when generating position files.+  duplicate_pin_numbers_are_jumpers+                        v10 only -> dropped. Always "no" across all 3609 stdlib symbols,+                        which matches v9's implicit behaviour, so normally a no-op. If a+                        library sets it to "yes" that ERC semantic has no v9 equivalent --+                        the tool warns rather than pretending otherwise.+  corner_radius         v10 only -> dropped (rounded rectangles become square)+  version               20251024 -> 20241209++  SYMBOL ORDER  Not a token change at all, and the one that actually bites. KiCad 9+                resolves (extends "Base") in a SINGLE FORWARD PASS while parsing, so a+                derived symbol placed before its base makes the entire library fail to+                load. KiCad 10 resolves the reference regardless of order, so v10 files+                are free to store the derived symbol first -- and real ones do. Convert+                such a file token-by-token and every token is correct, yet KiCad 9 still+                says only "Unable to load library", naming neither symbol. This tool+                topologically reorders symbols so each base precedes its dependants.+                Derived symbols are everywhere in practice (KiCad's own STM32 libraries+                are built on them), so a converter that ignores order works on simple+                passives and fails on almost every real IC.++Usage:+    python3 kicad_v10_to_v9.py in.kicad_sym [-o out.kicad_sym] [--quiet]++Validate the result with KiCad 9 itself -- that is the only authority that matters:+    kicad-cli sym export svg out.kicad_sym -o /tmp/svg+"""++from __future__ import annotations++import argparse+import sys+from pathlib import Path++V10 = "20251024"+V9 = "20241209"++# Direct children of (property ...) that v9 has no grammar for.+PROPERTY_DROP = {"show_name", "do_not_autoplace"}+# Direct children of (symbol ...) that v9 has no grammar for.+SYMBOL_DROP = {"in_pos_files", "duplicate_pin_numbers_are_jumpers"}+# Anywhere.+GLOBAL_DROP = {"corner_radius"}+++class Node(list):+    """An S-expression list. Atoms are plain str (quoted strings keep their quotes)."""+++def parse(text: str) -> Node:+    """Parse S-expressions. Handles quoted strings with backslash escapes."""+    i, n = 0, len(text)+    stack: list[Node] = [Node()]+    while i < n:+        c = text[i]+        if c.isspace():+            i += 1+        elif c == "(":+            node = Node()+            stack[-1].append(node)+            stack.append(node)+            i += 1+        elif c == ")":+            if len(stack) == 1:+                raise ValueError(f"unbalanced ')' at offset {i}")+            stack.pop()+            i += 1+        elif c == '"':+            j = i + 1+            while j < n:+                if text[j] == "\\":+                    j += 2+                    continue+                if text[j] == '"':+                    break+                j += 1+            stack[-1].append(text[i : j + 1])+            i = j + 1+        else:+            j = i+            while j < n and not text[j].isspace() and text[j] not in "()":+                j += 1+            stack[-1].append(text[i:j])+            i = j+    if len(stack) != 1:+        raise ValueError("unbalanced '(' -- unexpected end of input")+    return stack[0]+++def head(node) -> str | None:+    """The tag of an S-expression, e.g. 'property' for (property "Foo" ...)."""+    if isinstance(node, Node) and node and isinstance(node[0], str):+        return node[0]+    return None+++def dump(node, indent: int = 0) -> str:+    """Serialise back to KiCad's tab-indented style."""+    if not isinstance(node, Node):+        return str(node)+    tag = head(node)+    # Keep short leaf forms on one line, which is what KiCad does.+    if all(not isinstance(c, Node) for c in node):+        return "(" + " ".join(str(c) for c in node) + ")"+    pad = "\t" * indent+    out = ["(" + (tag or "")]+    inline = []+    rest = []+    for child in node[1:]:+        (inline if not isinstance(child, Node) and not rest else rest).append(child)+    if inline:+        out[0] += " " + " ".join(str(c) for c in inline)+    for child in rest:+        out.append(pad + "\t" + dump(child, indent + 1))+    return ("\n".join(out)) + "\n" + pad + ")"+++class Stats:+    def __init__(self):+        self.hide_moved = 0+        self.hide_dropped = 0+        self.dropped: dict[str, int] = {}+        self.jumpers_yes = 0+        self.reordered = 0++    def drop(self, tag: str):+        self.dropped[tag] = self.dropped.get(tag, 0) + 1+++def convert_property(prop: Node, st: Stats) -> Node:+    """Move (hide yes) into (effects ...) and drop v10-only children."""+    hide_yes = False+    kept: list = []+    for child in prop:+        tag = head(child)+        if tag == "hide":+            val = child[1] if len(child) > 1 else "yes"+            if val == "yes":+                hide_yes = True+                st.hide_moved += 1+            else:+                st.hide_dropped += 1+            continue+        if tag in PROPERTY_DROP:+            st.drop(tag)+            continue+        kept.append(child)++    out = Node(kept)+    if hide_yes:+        effects = next((c for c in out if head(c) == "effects"), None)+        if effects is None:+            effects = Node(["effects"])+            out.append(effects)+        # v9 puts (hide yes) inside (effects ...), after (font ...).+        if not any(head(c) == "hide" for c in effects):+            effects.append(Node(["hide", "yes"]))+    return out+++def walk(node, st: Stats, parent_tag: str | None = None):+    if not isinstance(node, Node):+        return node+    tag = head(node)++    if tag == "property":+        node = convert_property(node, st)++    kept = Node()+    for child in node:+        ctag = head(child)+        if ctag in GLOBAL_DROP:+            st.drop(ctag)+            continue+        if tag == "symbol" and ctag in SYMBOL_DROP:+            if ctag == "duplicate_pin_numbers_are_jumpers":+                val = child[1] if len(child) > 1 else "no"+                if val == "yes":+                    st.jumpers_yes += 1+            st.drop(ctag)+            continue+        if ctag == "version" and len(child) > 1 and child[1] == V10:+            child = Node(["version", V9])+        kept.append(walk(child, st, tag) if isinstance(child, Node) else child)+    return kept+++def order_symbols(root: Node, st: Stats) -> Node:+    """Emit base symbols BEFORE the symbols that (extends ...) them.++    KiCad 9 resolves `extends` in a single forward pass while parsing, so a derived+    symbol that appears before its base makes the whole library fail to load --+    `kicad-cli` reports only "Unable to load library", naming neither symbol. KiCad 10+    resolves the reference independently of order, so a v10 library is free to store+    the derived symbol first, and many do. Converting such a file token-by-token+    therefore produces valid-looking v9 that KiCad 9 still refuses.++    Stable: symbols that need no move keep their original relative order.+    """+    lib = root[0] if len(root) == 1 and head(root[0]) == "kicad_symbol_lib" else None+    if lib is None:+        return root++    syms = [c for c in lib[1:] if head(c) == "symbol"]+    if len(syms) < 2:+        return root++    def name_of(s):+        return s[1] if len(s) > 1 and isinstance(s[1], str) else None++    def base_of(s):+        for c in s:+            if head(c) == "extends" and len(c) > 1:+                return c[1]+        return None++    by_name = {name_of(s): s for s in syms}+    ordered, placed = [], set()++    def place(s, seen):+        sid = id(s)+        if sid in placed:+            return+        base = base_of(s)+        parent = by_name.get(base)+        # Missing base or a cycle: emit in place rather than guess.+        if parent is not None and id(parent) not in placed and id(parent) not in seen:+            place(parent, seen | {sid})+        if sid not in placed:+            placed.add(sid)+            ordered.append(s)++    for s in syms:+        place(s, set())++    if [id(s) for s in ordered] != [id(s) for s in syms]:+        st.reordered = sum(1 for a, b in zip(ordered, syms) if id(a) != id(b))+        out = Node([lib[0]])+        it = iter(ordered)+        for c in lib[1:]:+            out.append(next(it) if head(c) == "symbol" else c)+        return Node([out])+    return root+++def main() -> int:+    ap = argparse.ArgumentParser(description="Downgrade a KiCad 10 .kicad_sym to KiCad 9.")+    ap.add_argument("input")+    ap.add_argument("-o", "--output")+    ap.add_argument("--quiet", action="store_true")+    args = ap.parse_args()++    src = Path(args.input)+    text = src.read_text(encoding="utf-8-sig")   # tolerate a BOM; never write one back++    if f"(version {V10})" not in text.replace("\n", " ").replace("\t", " "):+        found = "unknown"+        for line in text.splitlines()[:8]:+            if "(version" in line:+                found = line.strip()+                break+        print(f"warning: input does not declare version {V10} (saw: {found})", file=sys.stderr)++    tree = parse(text)+    st = Stats()+    out = order_symbols(walk(tree, st), st)++    body = "\n".join(dump(child) for child in out) + "\n"+    dst = Path(args.output) if args.output else src.with_name(src.stem + "_v9.kicad_sym")+    dst.write_text(body, encoding="utf-8")++    if not args.quiet:+        print(f"wrote {dst}  ({dst.stat().st_size} bytes, from {src.stat().st_size})")+        print(f"  hide relocated into (effects ...): {st.hide_moved}")+        if st.hide_dropped:+            print(f"  (hide no) dropped: {st.hide_dropped}")+        for tag, count in sorted(st.dropped.items()):+            print(f"  dropped {tag}: {count}")+        if st.reordered:+            print(f"  reordered {st.reordered} symbol(s) so each (extends ...) base "+                  "comes first -- KiCad 9 resolves extends in one forward pass")+        if st.jumpers_yes:+            print(f"  !! {st.jumpers_yes} symbol(s) had duplicate_pin_numbers_are_jumpers=yes -- "+                  "that ERC semantic has NO v9 equivalent and is genuinely lost")+    return 0+++if __name__ == "__main__":+    sys.exit(main())
uninstall.sh+1−1
@@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail for HOME_DIR in "$HOME/.claude/skills" "$HOME/.codex/skills"; do-  rm -rf "$HOME_DIR/altium-bridge" "$HOME_DIR/altium-bridge-plan"+  rm -rf "$HOME_DIR/altium-bridge" "$HOME_DIR/altium-bridge-plan" "$HOME_DIR/altium-part-import" "$HOME_DIR/altium-kicad-import" done rm -rf "$HOME/.local/share/adom/altium-bridge" rm -f "$HOME/.local/bin/altium-dashboard"