app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
0.9.276: heal merge reads a corrupt orphan tolerantly: the orphaned Adom library on a redirected box had 94 symbols inside the library plus 28 duplicate copies appended AFTER the closing paren (the old appender bug's fingerprint), so the strict gate refused the whole file. The heal SOURCE is now read block by block with a quote-aware depth scan, each block balance-checked on its own, duplicates deduped first-copy-wins and reported; the file WRITTEN still goes through the strict gate. Measured on the real file: 94 unique recoverable, 0 unparseable, 28 dupes.
3 files changed
+105−7
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.9.275+0.9.276
adom_library.py+103−5@@ -424,6 +424,95 @@ def add_symbol(kicad_info: dict, symbol_text: str) -> dict: } ++def _library_close_index(text: str) -> int | None:+ """Byte index of the paren that closes the top-level (kicad_symbol_lib ...),+ found by a quote-aware depth scan. None if it never closes."""+ depth = 0+ in_str = False+ esc = False+ for i, ch in enumerate(text):+ if in_str:+ if esc:+ esc = False+ elif ch == "\\":+ esc = True+ elif ch == '"':+ in_str = False+ continue+ if ch == '"':+ in_str = True+ elif ch == "(":+ depth += 1+ elif ch == ")":+ depth -= 1+ if depth == 0:+ return i+ return None+++def _extract_symbols_tolerant(text: str) -> tuple[list[tuple[str, str]], list[str]]:+ """(symbols, unparseable) from a .kicad_sym that may carry top-level symbol+ blocks AFTER the library's closing paren. Each candidate block is checked to+ balance on its own; a block that does not (a truncated tail, say) is named+ in `unparseable` and skipped, never guessed at."""+ close = _library_close_index(text)+ inside = text[: close + 1] if close is not None else text+ trail = text[close + 1:] if close is not None else ""+ out: list[tuple[str, str]] = []+ bad: list[str] = []+ try:+ out.extend(_extract_top_level_symbols(inside))+ except Exception: # pylint: disable=broad-except+ pass+ if trail.strip():+ # BLOCK BY BLOCK, never all-or-nothing: the first version wrapped the whole+ # trailing region and parsed it once, so a single truncated last block made+ # every one of the 27 appended symbols "unparseable". Slice each top-level+ # (...) with the quote-aware depth scan; a block that closes is a candidate+ # (and still gets its own balance check); the unclosed tail is reported.+ pos = 0+ n = len(trail)+ while pos < n:+ start = trail.find("(", pos)+ if start < 0:+ break+ close = _library_close_index(trail[start:])+ if close is None:+ import re as _re+ m = _re.match(r'\(symbol\s+"([^"]+)"', trail[start:])+ bad.append(m.group(1) if m else "<unnamed block>")+ break+ block = trail[start:start + close + 1]+ pos = start + close + 1+ import re as _re+ m = _re.match(r'\(symbol\s+"([^"]+)"', block)+ if not m:+ continue # not a symbol block; ignore+ name = m.group(1)+ try:+ _parse_library_nodes("(kicad_symbol_lib\n" + block + "\n)")+ out.append((name, block))+ except Exception: # pylint: disable=broad-except+ bad.append(name)+ # DEDUPE, first copy wins. On John's laptop every one of the 28 appended+ # symbols was ALSO inside the library - the appender bug wrote each part+ # twice. The inside copy is what KiCad would load, so it is the one kept.+ seen: set[str] = set()+ unique: list[tuple[str, str]] = []+ dupes = 0+ for name, text in out:+ key = name.lower()+ if key in seen:+ dupes += 1+ continue+ seen.add(key)+ unique.append((name, text))+ if dupes:+ bad = bad + [f"<{dupes} duplicate copies skipped>"]+ return unique, bad++ def heal_split(kicad_info: dict, mode: str = "merge", dry_run: bool = False) -> dict: """Reconcile a split Adom library (wiki #71). John's call, 2026-09-03: MERGE. @@ -459,16 +548,24 @@ def heal_split(kicad_info: dict, mode: str = "merge", dry_run: bool = False) -> return {"success": False, "error": f"unknown mode {mode!r}", "allowed": ["merge", "repoint"]} try: orphan_text = orphan.read_text(encoding="utf-8")- _parse_library_nodes(orphan_text) # balance gate on the SOURCE too except Exception as e: # pylint: disable=broad-except- return {"success": False, "error": f"orphan library does not parse cleanly, refusing to merge from it: {e}"}+ return {"success": False, "error": f"cannot read orphan: {e}"}+ # TOLERANT SOURCE READ (John's laptop, 2026-09-03). The orphan failed the+ # strict gate: '(symbol "TJA1050" ...' after the library's closing paren -+ # the fingerprint of the old appender bug that once corrupted this very+ # library. 94 symbols sat inside the ')' and ~78KB of real symbols after it.+ # Refusing the whole file would strand every one of them. So the SOURCE is+ # read in two regions - inside the library, and trailing top-level blocks -+ # and EVERY block is balance-checked on its own before it is offered. The+ # strict gate still guards the file we WRITE (add_symbol's own check).+ src_symbols, unparseable = _extract_symbols_tolerant(orphan_text) have = {n.lower() for n in list_symbols(registered)}- to_add = [(name, text) for name, text in _extract_top_level_symbols(orphan_text)- if name.lower() not in have]- skipped = [name for name, _ in _extract_top_level_symbols(orphan_text) if name.lower() in have]+ to_add = [(name, text) for name, text in src_symbols if name.lower() not in have]+ skipped = [name for name, _ in src_symbols if name.lower() in have] if dry_run: return {"success": True, "healed": False, "mode": "merge", "dryRun": True, "wouldAdd": [n for n, _ in to_add], "alreadyPresent": skipped,+ "unparseableInSource": unparseable, "registeredPath": str(registered), "orphanPath": str(orphan)} added, failed = [], [] for name, text in to_add:@@ -481,6 +578,7 @@ def heal_split(kicad_info: dict, mode: str = "merge", dry_run: bool = False) -> after = library_split_report(kicad_info) return {"success": not failed, "healed": True, "mode": "merge", "dryRun": False, "added": added, "alreadyPresent": skipped, "failed": failed,+ "unparseableInSource": unparseable, "registeredSymbolsAfter": after.get("registeredSymbols"), "_hint": ("Merged into the file KiCad reads. A running KiCad sees them after its next " "library load (kicad_rescan_libraries force:true, or restart). The orphan file "
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "kicad", "displayName": "KiCad EDA",- "version": "0.9.275",+ "version": "0.9.276", "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",