app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
v0.1.5: Barrett consolidation — final parity verbs + the part-import and kicad-import skills in the master skillpack
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"