← All Pull Requests

0.2.13: Altium library refuses a symbol spec without pin types #45

Merged Released in 0.2.13 on 20 Oct 2026 opened by Colby Knox

adom-aiflow 0.2.13

  • Altium: a symbol tool without pin types is refused. altium-bridge 0.2.53 sets each library pin's electrical type from the KiCad symbol, but only when altium-symbol-modes writes the type into the spec. That tool lives in the container (from adom/altium-bridge) and updates separately from the bridge. In the 2026-10-20 end-to-end run, the container's copy was older than the bridge, so every TPS54202 pin went into Altium as Passive and the symbol check passed anyway.
    • altium library now refuses a spec whose PIN rows carry no pin type, and says to run adom-wiki pkg install adom/altium-bridge.
    • The schematic symbol check warns about any kept spec without pin types (parts imported earlier) and says how to re-import it.

Tests: tests/altium-lane.py 17 OK (2 new); cargo test all green. Checked live: the laptop's updated altium-symbol-modes writes power_in/power_out/input for the TPS54202.

🤖 Generated with Claude Code

Diff Skip to comments

--- a/tools/altium-project.py+++ b/tools/altium-project.py@@ -1,711 +1,717 @@⋯ 95 unchanged lines ⋯     if use:         r = run(['altium-symbol-modes', str(use), str(spec_sym), key, src['desc']])         if r.returncode != 0 or not spec_sym.is_file(): err('%s: symbol conversion failed: %s' % (label, clip(r.stdout + r.stderr, 300)))+        # PIN|mode|number|name|x|y|orient|len|hide|etype: a spec without the pin type comes from a tool older+        # than altium-bridge 0.2.53, and every pin would land in Altium as Passive (a 2026-10-20 run did)+        if any(len(l.split('|')) < 10 for l in spec_sym.read_text(errors='ignore').splitlines() if l.startswith('PIN|')):+            err('%s: altium-symbol-modes is older than altium-bridge 0.2.53: its symbol spec carries no pin types, so every pin would go into Altium as Passive' % label,+                ['Update it: adom-wiki pkg install adom/altium-bridge, then run this again.'])         files.append(spec_sym)     if src.get('step'): files.append(src['step'])     return files, (spec_sym if spec_sym in files else None)⋯ 607 unchanged lines ⋯   main()+--- a/tools/altium-lane.py+++ b/tools/altium-lane.py@@ -1,499 +1,503 @@⋯ 55 unchanged lines ⋯             elif f[0] == 'ARC': mode(int(f[1]))['arcs'].append(tuple(float(v) for v in f[2:7]))             elif f[0] == 'PIN':                 mode(int(f[1]))['pins'].append({'number': f[2], 'name': f[3], 'x': float(f[4]), 'y': float(f[5]), 'orient': f[6],-                                                 'len': float(f[7]), 'hidden': len(f) > 8 and f[8] == 'hide'})+                                                 'len': float(f[7]), 'hidden': len(f) > 8 and f[8] == 'hide', 'etype': f[9] if len(f) > 9 else None})         except (ValueError, IndexError):             continue     return sym⋯ 280 unchanged lines ⋯                 sp = None         if not sp:             checked['noSpec'].append(lr)+        elif any(p['etype'] is None for m in sp['modes'].values() for p in m['pins']):+            # an altium-symbol-modes older than altium-bridge 0.2.53 wrote no pin types: every pin went in Passive+            warnings.append('library: %s: its kept spec carries no pin types (altium-symbol-modes older than altium-bridge 0.2.53), so its pins are Passive in Altium; `adom-wiki pkg install adom/altium-bridge`, then `altium library --only <MPN>` imports it again with them' % who)         # 2. pins on the body edge, every display mode of the spec         if sp:             for i, m in sorted(sp['modes'].items()):⋯ 148 unchanged lines ⋯     desc = prov.get('description') or (row or {}).get('value') or pl.get('footprint') or ''     return {'desc': str(desc)[:120].replace('\n', ' '), 'mfr': prov.get('manufacturer') or (row or {}).get('manufacturer') or '',             'package': prov.get('package') or pl.get('footprint') or '', 'links': links, 'source': pl.get('source') or prov.get('source') or ''}+--- a/tests/altium-lane.py+++ b/tests/altium-lane.py@@ -1,372 +1,403 @@⋯ 36 unchanged lines ⋯ PIN|1|5|EN|800|-200|right|300 PIN|1|6|BOOT|800|-300|right|300 """+def typed(spec, etype='passive'):+    """a spec as altium-symbol-modes writes it since altium-bridge 0.2.53: every PIN row ends hide|etype"""+    rows = []+    for l in spec.splitlines():+        f = l.split('|')+        if f[0] == 'PIN': f = (f + [''] * 9)[:9] + [etype]+        rows.append('|'.join(f))+    return '\n'.join(rows) + '\n'+++IC_UNTYPED = IC_GOOD  # as an altium-symbol-modes before altium-bridge 0.2.53 wrote it: no pin types+IC_GOOD = typed(IC_GOOD) # the same symbol as symbol_modes wrote it before 2026-10-19: every pin's OUTER end, one pin length off the box IC_BAD = IC_GOOD.replace('|0|-100|left', '|-300|-100|left').replace('|0|-200|left', '|-300|-200|left').replace('|0|-300|left', '|-300|-300|left').replace('|800|-', '|1100|-') ⋯ 8 unchanged lines ⋯ PIN|0|1|K|-50|0|left|100|hide PIN|0|2|A|50|0|right|100|hide """+DIODE_GOOD = typed(DIODE_GOOD) # the drawing mirrored, pins kept: the triangle and the bar point at the anode DIODE_BAD = """META|D-KEY|diode|D MODE|0|native⋯ 114 unchanged lines ⋯         rep = lane.check_symbols([placed('Q1', 'Q', flipped)], {'Q': flipped}, {'Q': prims(flipped)})         self.assertIn('emitter arrow is drawn on the collector lead', ' '.join(rep['errors'])) +    def test_a_kept_spec_without_pin_types_is_named(self):+        rep = lane.check_symbols([placed('U1', 'TPS54202DDCR', IC_UNTYPED, types=POWER)], {'TPS54202DDCR': IC_UNTYPED}, {'TPS54202DDCR': prims(IC_UNTYPED)})+        self.assertTrue(rep['ok'], rep)+        self.assertEqual(len(rep['warnings']), 1, rep['warnings'])+        self.assertIn('carries no pin types', rep['warnings'][0])+        self.assertIn('altium library --only', rep['warnings'][0])+     def test_no_kept_spec_checks_the_drawn_mode_only(self):         rep = lane.check_symbols([placed('U1', 'TPS54202DDCR', IC_BAD, types=POWER)], {}, {'TPS54202DDCR': dict(prims(IC_BAD), bodyRectMil=[0, -400, 800, 0])})         self.assertFalse(rep['ok'])⋯ 99 unchanged lines ⋯ import json, os, sys src = open(sys.argv[1]).read() with open(os.environ["FAKE_LOG"], "a") as f: f.write(json.dumps({"tool": "symbol-modes", "argv": sys.argv[1:], "pins": src.count("(pin ")}) + "\n")-open(sys.argv[2], "w").write("META|%s|%s|U\nMODE|0|std\nRECT|0|0|-200|600|0\nPIN|0|1|IN|0|-100|left|300\n" % (sys.argv[3], sys.argv[4]))+open(sys.argv[2], "w").write("META|%s|%s|U\nMODE|0|std\nRECT|0|0|-200|600|0\nPIN|0|1|IN|0|-100|left|300||input\n" % (sys.argv[3], sys.argv[4])) '''  FAKE_WIKI = r'''#!/usr/bin/env python3⋯ 4 unchanged lines ⋯ class LibraryCommand(unittest.TestCase):     """`altium library` end to end with fake CLIs: a part with no wiki page, given by the project library.""" -    def test_project_library_part_is_imported(self):+    def test_a_symbol_tool_without_pin_types_is_refused(self):+        # altium-symbol-modes before altium-bridge 0.2.53 wrote no pin types: the import would make every pin Passive+        old = FAKE_MODES.replace('|300||input\\n', '|300\\n')+        self.assertNotEqual(old, FAKE_MODES)+        r = self.test_project_library_part_is_imported(old)+        self.assertEqual(r.returncode, 1, r.stdout)+        self.assertIn('altium-symbol-modes is older than altium-bridge 0.2.53', r.stdout)+        self.assertIn('adom-wiki pkg install adom/altium-bridge', r.stdout)++    def test_project_library_part_is_imported(self, modes=None):         with tempfile.TemporaryDirectory() as t:             d = Path(t); b = d / 'bin'; b.mkdir()-            for name, src in (('adom-bridge', FAKE_BRIDGE), ('altium-kicad2altium', FAKE_K2A), ('altium-symbol-modes', FAKE_MODES), ('adom-wiki', FAKE_WIKI)):+            for name, src in (('adom-bridge', FAKE_BRIDGE), ('altium-kicad2altium', FAKE_K2A), ('altium-symbol-modes', modes or FAKE_MODES), ('adom-wiki', FAKE_WIKI)):                 (b / name).write_text(src); (b / name).chmod(0o755)             run = d / 'run'; run.mkdir()             pl = project_lib(d / 'lib')⋯ 4 unchanged lines ⋯             log = d / 'log.jsonl'             env = dict(os.environ, PATH='%s:%s:/usr/bin:/bin' % (b, os.path.dirname(sys.executable)), AIFLOW_RUN=str(run), AIFLOW_TARGET='box', AIFLOW_BOX_DIR='C:/Users/t/Documents/AdomAiFlow/run', FAKE_LOG=str(log))             r = subprocess.run([sys.executable, str(script), 'library', '--parts', str(net), '--name', 'Board'], capture_output=True, text=True, env=env, stdin=subprocess.DEVNULL)+            if modes: return r             self.assertEqual(r.returncode, 0, r.stdout + r.stderr)             self.assertIn('1 from the board\'s project library: ABC-123', r.stdout)             calls = [json.loads(l) for l in log.read_text().splitlines()]⋯ 63 unchanged lines ⋯  if __name__ == '__main__':     unittest.main(verbosity=1)+--- a/docs/release-0.2.13.md+++ b/docs/release-0.2.13.md@@ -0,0 +1,6 @@+# adom-aiflow 0.2.13++- **Altium: a symbol tool without pin types is refused.** altium-bridge 0.2.53 sets each library pin's electrical type from the KiCad symbol, but only when `altium-symbol-modes` writes the type into the spec. That tool lives in the container (from `adom/altium-bridge`) and updates separately from the bridge. In the 2026-10-20 end-to-end run, the container's copy was older than the bridge, so every TPS54202 pin went into Altium as Passive and the symbol check passed anyway.+  - `altium library` now refuses a spec whose PIN rows carry no pin type, and says to run `adom-wiki pkg install adom/altium-bridge`.+  - The schematic symbol check warns about any kept spec without pin types (parts imported earlier) and says how to re-import it.+--- a/package.json+++ b/package.json@@ -1,94 +1,95 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.12",+  "version": "0.2.13",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",⋯ 84 unchanged lines ⋯   ],   "license": "MIT" }+--- a/page.json+++ b/page.json@@ -1,100 +1,100 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.2.12",+  "version": "0.2.13",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",⋯ 93 unchanged lines ⋯--- a/Cargo.toml+++ b/Cargo.toml@@ -1,40 +1,41 @@⋯ 13 unchanged lines ⋯ ]  [workspace.package]-version = "0.2.12"+version = "0.2.13" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"⋯ 17 unchanged lines ⋯ lto = true codegen-units = 1 strip = true+--- a/Cargo.lock+++ b/Cargo.lock@@ -1,648 +1,649 @@⋯ 3 unchanged lines ⋯  [[package]] name = "adom-aiflow"-version = "0.2.12"+version = "0.2.13" dependencies = [  "aiflow-analyze",  "aiflow-board",⋯ 14 unchanged lines ⋯  [[package]] name = "aiflow-analyze"-version = "0.2.12"+version = "0.2.13" dependencies = [  "aiflow-board",  "serde",⋯ 3 unchanged lines ⋯  [[package]] name = "aiflow-board"-version = "0.2.12"+version = "0.2.13" dependencies = [  "roxmltree",  "serde",⋯ 2 unchanged lines ⋯  [[package]] name = "aiflow-bridge"-version = "0.2.12"+version = "0.2.13" dependencies = [  "aiflow-board",  "serde",⋯ 2 unchanged lines ⋯  [[package]] name = "aiflow-copper"-version = "0.2.12"+version = "0.2.13" dependencies = [  "aiflow-board",  "aiflow-grid",⋯ 3 unchanged lines ⋯  [[package]] name = "aiflow-grid"-version = "0.2.12"+version = "0.2.13" dependencies = [  "aiflow-board",  "serde",⋯ 2 unchanged lines ⋯  [[package]] name = "aiflow-place"-version = "0.2.12"+version = "0.2.13" dependencies = [  "aiflow-board",  "serde",⋯ 2 unchanged lines ⋯  [[package]] name = "aiflow-pours"-version = "0.2.12"+version = "0.2.13" dependencies = [  "aiflow-board",  "aiflow-copper",⋯ 3 unchanged lines ⋯  [[package]] name = "aiflow-router"-version = "0.2.12"+version = "0.2.13" dependencies = [  "aiflow-board",  "aiflow-grid",⋯ 3 unchanged lines ⋯  [[package]] name = "aiflow-run"-version = "0.2.12"+version = "0.2.13" dependencies = [  "serde",  "serde_json",⋯ 538 unchanged lines ⋯ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"+

Comments

No comments yet.

Log in to comment.