app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
John Lauer
Publish 0.2.25
68f933c
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
"""route --variants N and route --pick K --why on a small synthetic board (issue #55). A fake
kicad-cli answers DRC with no violations, so the gate column is exercised without CAD; no
dashboard, no bridge. Usage: python3 tests/route-variants-workflow.py <adom-aiflow binary>"""
import hashlib, json, os, pathlib, subprocess, sys, tempfile
binary = str(pathlib.Path(sys.argv[1]).resolve())
def board_text():
pads = [("J1", "A", 2, 2), ("J2", "A", 6, 2), ("J3", "B", 2, 8), ("J4", "B", 18, 8), ("J5", "C", 10, 2), ("J6", "C", 14, 6)]
t = '(kicad_pcb\n\t(version 20260206)\n\t(generator "test")\n\t(layers\n\t\t(0 "F.Cu" signal)\n\t\t(2 "B.Cu" signal)\n\t\t(25 "Edge.Cuts" user)\n\t\t(31 "F.CrtYd" user)\n\t)\n'
for r, net, x, y in pads:
t += f'\t(footprint "T:{r}"\n\t\t(layer "F.Cu")\n\t\t(at {x} {y})\n\t\t(property "Reference" "{r}")\n\t\t(fp_rect\n\t\t\t(start -0.9 -0.9)\n\t\t\t(end 0.9 0.9)\n\t\t\t(stroke\n\t\t\t\t(width 0.05)\n\t\t\t\t(type solid)\n\t\t\t)\n\t\t\t(fill no)\n\t\t\t(layer "F.CrtYd")\n\t\t)\n\t\t(pad "1" thru_hole circle\n\t\t\t(at 0 0)\n\t\t\t(size 1.2 1.2)\n\t\t\t(drill 0.6)\n\t\t\t(layers "*.Cu")\n\t\t\t(net "{net}")\n\t\t)\n\t)\n'
t += '\t(gr_rect\n\t\t(start 0 0)\n\t\t(end 20 10)\n\t\t(layer "Edge.Cuts")\n\t)\n)\n'
return t
with tempfile.TemporaryDirectory(prefix='aiflow-variants-') as td:
p = pathlib.Path(td)
(p / 'b.kicad_pcb').write_text(board_text())
(p / 'spec.json').write_text(json.dumps({"board": "variants fixture", "clearance": 0.2}))
fake = p / 'kicad-cli'
fake.write_text('#!/usr/bin/env python3\nimport sys, json\na = sys.argv[1:]\n'
'if "--help" in a: print("--refill-zones"); sys.exit(0)\n'
'out = a[a.index("--output") + 1]\njson.dump({"violations": [], "unconnected_items": []}, open(out, "w"))\n')
fake.chmod(0o755)
run = p / 'run'
env = dict(os.environ, AIFLOW_DASHBOARD='off', ADOM_AIFLOW_KICAD_CLI=str(fake))
env.pop('KICAD_REMOTE_TARGET', None)
def call(*args):
return subprocess.run([binary, '--run', str(run), '--ai-thread', 'variants-test', *args], env=env, capture_output=True, text=True, timeout=120)
def sha(f):
return hashlib.sha256(pathlib.Path(f).read_bytes()).hexdigest()
r = call('start', '--board', str(p / 'b.kicad_pcb'), '--spec', str(p / 'spec.json'), '--engine', 'test')
assert r.returncode == 0, r.stderr
# a plain route: no variants on the outcome
r = call('route'); assert r.returncode == 0, r.stdout + r.stderr
plain = sha(run / 'plan-route.json')
o = json.loads((run / 'run.json').read_text())['outcomes']['route']
assert 'variants' not in o, o
# --pick before any variants is refused
assert call('route', '--pick', '1', '--why', 'x').returncode != 0
# three variants: files, scores, a pick by the gates, the table
r = call('route', '--variants', '3'); assert r.returncode == 0, r.stdout + r.stderr
if os.environ.get('SHOW'): print(r.stderr)
assert 'chosen: v' in r.stderr and 'levers' in r.stderr, r.stderr
o = json.loads((run / 'run.json').read_text())['outcomes']['route']
v = o['variants']
assert v['count'] == 3 and v['chosenBy'] == 'the gates' and v['drc'].startswith('kicad-cli'), v
for k, x in enumerate(v['variants'], 1):
assert x['variant'] == k and (run / f'plan-route-v{k}.json').is_file() and (run / f'plan-route-v{k}-report.json').is_file()
assert x['gate'] is not None and x['gate']['issues'] == 0, x
assert x['levers']['order'] and x['copperLengthMm'] > 0 and x['rank'] >= 1
assert sorted(x['rank'] for x in v['variants']) == [1, 2, 3]
win = v['chosen']
assert [x for x in v['variants'] if x['rank'] == 1][0]['variant'] == win
assert sha(run / 'plan-route.json') == sha(run / f'plan-route-v{win}.json')
# variant 1 is a plain route: the same plan
assert sha(run / 'plan-route-v1.json') == plain
# deterministic: the same variants again give the same plans
before = [sha(run / f'plan-route-v{k}.json') for k in (1, 2, 3)]
assert call('route', '--variants', '3').returncode == 0
assert before == [sha(run / f'plan-route-v{k}.json') for k in (1, 2, 3)]
# the ledger holds every variant
led = [json.loads(l) for l in (run / 'run.jsonl').read_text().splitlines()]
assert any(e.get('event') == 'route-variants' and len(e['variants']) == 3 for e in led)
# --pick needs --why, and records it
other = 1 if win != 1 else 2
assert call('route', '--pick', str(other)).returncode != 0
assert call('route', '--pick', '9', '--why', 'none such').returncode != 0
r = call('route', '--pick', str(other), '--why', 'the shorter loop'); assert r.returncode == 0, r.stdout + r.stderr
o = json.loads((run / 'run.json').read_text())['outcomes']['route']
assert o['variants']['chosen'] == other and o['variants']['chosenBy'] == 'the AI' and o['variants']['why'] == 'the shorter loop'
assert o['variants']['gatePick'] == win
assert sha(run / 'plan-route.json') == sha(run / f'plan-route-v{other}.json')
led = [json.loads(l) for l in (run / 'run.jsonl').read_text().splitlines()]
assert any(e.get('event') == 'route-pick' and e['why'] == 'the shorter loop' for e in led)
# the gate takes the picked plan as usual
r = call('gate'); assert r.returncode == 0, r.stdout + r.stderr
# a plain route again: the baseline plan, no variants
assert call('route').returncode == 0
assert sha(run / 'plan-route.json') == plain
assert 'variants' not in json.loads((run / 'run.json').read_text())['outcomes']['route']
print('PASS: route --variants writes, gates and ranks every variant, the winner is the route plan, v1 equals a plain route, reruns are identical, --pick needs --why and is recorded, plain route unchanged')