← Commit history

AI Flow 0.1.64: Fusion library names uppercase (ADD cannot find a lowercase part name); a stranded native Fusion dialog is named when Fusion reports busy

John Lauer ·420d94e5e3 · ·parent 8994e44
7 files changed +45−29
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.63"+version = "0.1.64" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.63"+version = "0.1.64" dependencies = [  "serde",  "serde_json",@@ -33,7 +33,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.63"+version = "0.1.64" dependencies = [  "roxmltree",  "serde",@@ -42,7 +42,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.63"+version = "0.1.64" dependencies = [  "aiflow-board",  "serde",@@ -51,7 +51,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.63"+version = "0.1.64" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -61,7 +61,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.63"+version = "0.1.64" dependencies = [  "aiflow-board",  "serde",@@ -70,7 +70,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.63"+version = "0.1.64" dependencies = [  "aiflow-board",  "serde",@@ -79,7 +79,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.63"+version = "0.1.64" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -89,7 +89,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.63"+version = "0.1.64" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -99,7 +99,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.63"+version = "0.1.64" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.63"+version = "0.1.64" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
docs/release-0.1.64.mdadded+4
@@ -0,0 +1,4 @@+# AI Flow 0.1.64++- **Fusion library names are uppercase.** Fusion's ADD command cannot find a part whose library name has lowercase letters: the 0.5 mm test point (`Adom_TestPoint_D0.5mm`) failed to place on every attempt while every all-uppercase MPN placed, and a take of the buck demo fell back to the old 1.0 mm pad (no 3D, so a black box). `fusion library` now gives adom-lbr uppercase device and package names, binds the 3D under them, and every package-name lookup (3D injection, the board's restore, the missing-3D check) compares without case. ORIENT_RULES 3 rebuilds parts made under the old names. The MPN attribute and the wiki page are unchanged.+- **A stranded native dialog is named.** When Fusion reports its main thread busy, `fusion` steps look for a native Open, Save or Export window left by an interrupted step and say so in the hint; the bridge cannot see those dialogs, and the run otherwise waits on a Fusion that will not answer.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.63",+  "version": "0.1.64",   "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 today; Altium, Fusion and 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.",
page.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.63",+  "version": "0.1.64",   "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 today; Altium, Fusion and 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.",
tools/fusion-project.py+28−16
@@ -113,8 +113,19 @@ def ab(verb, args, timeout=600):     except Exception: return {'success': False, 'error': (p.stdout + p.stderr)[-400:]}     o = d.get('output')     if isinstance(o, str) and o.startswith('{'):-        try: return json.loads(o)+        try: d = json.loads(o)         except Exception: pass+    # Fusion busy for a long time is often a native file dialog (Open, Save, Export) left on screen by an+    # interrupted step: the bridge cannot see it, so name it (2026-10-07: a stopped schematic left "Open")+    if verb.startswith('fusion_') and 'busy' in str(d.get('error') or d.get('errorCode') or '').lower():+        w = run(['adom-bridge', '--target', TARGET, '--ai-thread', THREAD, 'desktop_list_windows', '{}'], timeout=60)+        try:+            wd = json.loads(w.stdout); wo = json.loads(wd['output']) if str(wd.get('output', '')).startswith('{') else wd+            stuck = [x.get('title') for x in ((wo.get('data') or wo).get('windows') or []) if x.get('image') == 'Fusion360.exe' and (x.get('title') or '').strip() in ('Open', 'Save', 'Save As', 'Export', 'Import')]+        except Exception:+            stuck = []+        if stuck:+            d['_hint'] = 'Fusion has a native %s dialog open (left by an interrupted step); it blocks every command. Cancel it on the desktop (or ask the human to), then retry.' % ' / '.join(stuck)     return d  @@ -556,7 +567,7 @@ def service_pose(step, fp):     return None  -ORIENT_RULES = 2  # bump when the orientation rules change: parts bound under older rules are bound again+ORIENT_RULES = 3  # 3: Fusion library names are uppercase (ADD cannot find a lowercase name). Bump when the orientation rules change: parts bound under older rules are bound again   def silk_text(t):@@ -636,7 +647,7 @@ def cmd_library(a):         pdir = STATE / 'pages' / re.sub(r'[^A-Za-z0-9._-]', '_', mpn)         out_lbr = lib_dir / 'parts' / (mpn + '.lbr')         if w.get('footprint'):  # a board-only part (test pad) from a footprint file-            r = run(['adom-lbr', 'generate', '--fp', w['footprint'], '--name', mpn, '--package-name', mpn, '-o', str(out_lbr)])+            r = run(['adom-lbr', 'generate', '--fp', w['footprint'], '--name', mpn.upper(), '--package-name', mpn.upper(), '-o', str(out_lbr)])             if 'Lint PASSED' not in r.stdout + r.stderr: err('%s: adom-lbr generate failed: %s' % (mpn, (r.stdout + r.stderr)[-300:]))             st['parts'][mpn] = {'lbr': str(out_lbr), 'step': None, 'boardOnly': True, 'numbers': {}, 'lbrVersion': lbr_ver}             added.append(mpn); say('part %s: board only (footprint %s)' % (mpn, Path(w['footprint']).name)); continue@@ -658,7 +669,7 @@ def cmd_library(a):         elif choice in DEVICE_SYMBOLS:             use = STATE / 'symbols' / (choice + '.kicad_sym'); use.parent.mkdir(parents=True, exist_ok=True); use.write_text(DEVICE_SYMBOLS[choice])         else: use = sym-        args = ['adom-lbr', 'generate', '--fp', str(fp), '--part-metadata', str(pdir / 'page.meta.json'), '--name', mpn, '--package-name', mpn, '-o', str(out_lbr)]+        args = ['adom-lbr', 'generate', '--fp', str(fp), '--part-metadata', str(pdir / 'page.meta.json'), '--name', mpn.upper(), '--package-name', mpn.upper(), '-o', str(out_lbr)]         if use: args[2:2] = ['--sym', str(use)]         if choice in DEVICE_SYMBOLS: args += ['--symbol-layout', 'keep']         r = run(args)@@ -700,27 +711,28 @@ def cmd_library(a):         send(sorted(stage.glob('lib/*')), bdir + '/lib'); send(sorted(stage.glob('models/*')), bdir + '/models'); send([combined], bdir)         say('binding real 3D to %d part(s) in Fusion (about a minute each)' % len(todo))         r = ab('fusion_build_library_3d', {'lbrPath': bdir + '/' + combined.name, 'outLbrPath': bdir + '/' + name + '_3d.lbr', 'capture': True,-                                           'parts': [{'package': m, 'lbrPath': bdir + '/lib/' + m + '.lbr', 'modelPath': bdir + '/models/' + m + '.step'} for m in todo]}, timeout=300)+                                           'parts': [{'package': m.upper(), 'lbrPath': bdir + '/lib/' + m + '.lbr', 'modelPath': bdir + '/models/' + m + '.step'} for m in todo]}, timeout=300)         t0 = time.time(); res = {}         while time.time() - t0 < 60 * (5 + 3 * len(todo)):             time.sleep(20)             pr = ab('fusion_prove_result', {}).get('result') or {}-            if pr.get('boundLbr', '').endswith(name + '_3d.lbr') and pr.get('partsDone') == len(todo) and all(x.get('package') in todo for x in pr.get('parts', [])):+            if pr.get('boundLbr', '').endswith(name + '_3d.lbr') and pr.get('partsDone') == len(todo) and all((x.get('package') or '').upper() in {m.upper() for m in todo} for x in pr.get('parts', [])):                 res = pr; break             if pr.get('stage'): say('  3D: %s' % pr.get('stage'))         if not res: err('the 3D build did not finish in time', ['Read fusion_prove_result; run `adom-aiflow fusion library` again: parts already bound are kept and only the rest are built.'])+        back = {m.upper(): m for m in st['parts']}         for x in res.get('parts', []):-            if x.get('success') and x.get('wip_urn'): st['parts'][x['package']]['urn'] = x['wip_urn']+            if x.get('success') and x.get('wip_urn'): st['parts'][back.get(x['package'].upper(), x['package'])]['urn'] = x['wip_urn']         save_state(st)     # inject every binding (this build's and earlier ones) into the library, then put it on the box-    urns = {m: p['urn'] for m, p in st['parts'].items() if p.get('urn')}+    urns = {m.upper(): p['urn'] for m, p in st['parts'].items() if p.get('urn')}  # keyed by Fusion's (uppercase) package name     t = combined.read_text()     t = re.sub(r'(?s)<packages3d>.*?</packages3d>', '', t); t = re.sub(r'(?s)<package3dinstances>.*?</package3dinstances>', '', t)     p3 = ''.join('<package3d name="%s" urn="" wip_urn="%s" locally_modified="yes" type="model"><description>%s</description><packageinstances><packageinstance name="%s"/></packageinstances></package3d>' % (m, u, m, m) for m, u in urns.items())     if p3: t = t.replace('</packages>', '</packages><packages3d>' + p3 + '</packages3d>', 1)     def dev(mt):         d = mt.group(0); pk = re.search(r'package="([^"]+)"', d.split('>', 1)[0])-        u = urns.get(pk.group(1)) if pk else None+        u = urns.get(pk.group(1).upper()) if pk else None         return d.replace('</connects>', '</connects><package3dinstances><package3dinstance package3d_urn="%s"/></package3dinstances>' % u, 1) if u else d     t = re.sub(r'(?s)<device [^>]*package="[^"]+"[^>]*>.*?</device>', dev, t)     combined.write_text(t); send([combined], bdir)@@ -861,13 +873,13 @@ def cmd_schematic(a):     if n != len(nl['components']): err('the board has %s of %d parts: schematic/board sync is off' % (n, len(nl['components'])), ['Start from a clean Fusion session (close half-finished imported designs) and run this again.'])     # 3D on the board: a part added live arrives without the library's 3D binding (Fusion then draws a flat     # placeholder box in the 3D PCB). Count them and put them back before the design is saved.-    want3d = {m for m, p_ in st['parts'].items() if p_.get('urn')}+    want3d = {m.upper() for m, p_ in st['parts'].items() if p_.get('urn')}     def missing3d():         cur = ab('fusion_get_app_state', {}); cur = (cur.get('data') or cur).get('activeDocument') or name         sb = ab('fusion_board_source', {'expectDocument': cur})         if not sb.get('source'): err('could not read the board to check its 3D: %s' % sb.get('error'), ['Show the board (fusion_show_2d_board) and run this again.'])         return sorted(re.search(r'name="([^"]+)"', e).group(1) for e in re.findall(r'<element [^>]*>', sb.get('source') or '')-                      if (re.search(r'package="([^"]+)"', e) or [None, None])[1] in want3d and 'package3d_urn' not in e)+                      if ((re.search(r'package="([^"]+)"', e) or [None, ''])[1] or '').upper() in want3d and 'package3d_urn' not in e)     lost = missing3d(); restored = []     if lost:         restored = restore_3d(st, name, a.project, a.folder)@@ -895,7 +907,7 @@ def restore_3d(st, name, project, folder):     flat placeholder box). The design is exported, every schematic part and board element of a package that     has a model gets its package3d_urn, the library gets its packages3d, and the design is imported again.     Returns the parts that were restored."""-    urns = {m: p['urn'] for m, p in st['parts'].items() if p.get('urn')}+    urns = {m.upper(): p['urn'] for m, p in st['parts'].items() if p.get('urn')}  # keyed by Fusion's (uppercase) package name     lib = st['library']; bdir = box_dir() + '/design'; loc = STATE / 'restore3d'; loc.mkdir(parents=True, exist_ok=True)     for ext in ('sch', 'brd'):         r = ab('fusion_export_eagle_source', {'outputPath': '%s/%s-src.%s' % (bdir, name, ext)})@@ -910,17 +922,17 @@ def restore_3d(st, name, project, folder):             L = L.replace('</packages>', '</packages>' + p3, 1)             def dev(md):                 D = md.group(0); pk = re.search(r'package="([^"]+)"', D.split('>', 1)[0])-                if not pk or pk.group(1) not in urns or 'package3dinstances' in D: return D-                return D.replace('</connects>', '</connects><package3dinstances><package3dinstance package3d_urn="%s"/></package3dinstances>' % urns[pk.group(1)], 1)+                if not pk or pk.group(1).upper() not in urns or 'package3dinstances' in D: return D+                return D.replace('</connects>', '</connects><package3dinstances><package3dinstance package3d_urn="%s"/></package3dinstances>' % urns[pk.group(1).upper()], 1)             return re.sub(r'(?s)<device [^>]*>.*?</device>', dev, L)         return re.sub(r'(?s)<library [^>]*>.*?</library>', one, t)     restored = set()     brd = fix_lib((loc / (name + '-src.brd')).read_text())     def el(mt):         e = mt.group(0); pk = re.search(r'package="([^"]+)"', e)-        if 'package3d_urn' in e or not pk or pk.group(1) not in urns: return e+        if 'package3d_urn' in e or not pk or pk.group(1).upper() not in urns: return e         restored.add(re.search(r'name="([^"]+)"', e).group(1))-        return e[:-1].rstrip('/') + ' package3d_urn="%s"' % urns[pk.group(1)] + ('/>' if e.endswith('/>') else '>')+        return e[:-1].rstrip('/') + ' package3d_urn="%s"' % urns[pk.group(1).upper()] + ('/>' if e.endswith('/>') else '>')     brd = re.sub(r'<element [^>]*>', el, brd)     sch = fix_lib((loc / (name + '-src.sch')).read_text())     devpkg = {}