main
John Lauer Publish 0.2.48 7fce739
"""Issue #47: rules install honours --layers, never silently replaces another layer pack, and
rules check falls back to run.json fabRules and judges jlcpcb by the board's copper layers."""
import json,os,pathlib,subprocess,sys,tempfile
binary=str(pathlib.Path(sys.argv[1]).resolve())
SIX='(kicad_pcb (version 20260101) (layers (0 "F.Cu" signal) (1 "In1.Cu" signal) (2 "In2.Cu" signal) (3 "In3.Cu" signal) (4 "In4.Cu" signal) (31 "B.Cu" signal) (44 "Edge.Cuts" user)) (gr_rect (start 0 0) (end 10 10) (layer "Edge.Cuts")))\n'
PACK6='# adom-aiflow rules: jlcpcb 6L (from adom/pcb-design-rules)\n(version 1)\n(rule "x" (constraint clearance (min 0.1mm)))\n'
with tempfile.TemporaryDirectory(prefix='aiflow-rules-layers-') as td:
 p=pathlib.Path(td);run=p/'run';env=dict(os.environ,AIFLOW_DASHBOARD='off')
 def call(*args,inrun=True):
  pre=['--run',str(run)] if inrun else ['--run',str(p/'none')]
  return subprocess.run([binary,*pre,'--ai-thread','rules-layers',*args],env=env,capture_output=True,text=True,timeout=30)
 board=p/'b6.kicad_pcb';board.write_text(SIX);dru=board.with_suffix('.kicad_dru')
 # 1. install with --layers 6: no shipped pack, an error that points at `rules use`, nothing written
 r=call('rules','install','--profile','jlcpcb','--layers','6','--board',str(board),'--force',inrun=False)
 assert r.returncode!=0 and 'rules use --profile jlcpcb --layers 6' in (r.stdout+r.stderr),(r.stdout,r.stderr)
 assert not dru.exists()
 # without --layers the board's 6 copper layers decide: still not the 2L pack
 r=call('rules','install','--profile','jlcpcb','--board',str(board),inrun=False);assert r.returncode!=0 and not dru.exists()
 # --layers 2 on a 6-layer board is refused
 r=call('rules','install','--profile','jlcpcb','--layers','2','--board',str(board),'--force',inrun=False);assert r.returncode!=0 and not dru.exists()
 # 2. the 6L pack passes as jlcpcb and jlcpcb-6L; the 2L file fails with the right command
 dru.write_text(PACK6)
 for prof in ['jlcpcb','jlcpcb-6L']:
  r=call('rules','check','--profile',prof,'--board',str(board),inrun=False);assert r.returncode==0,(prof,r.stdout,r.stderr)
 # 3. install on a 2-layer board over a 6L pack: --force replaces it and says so, keeping a .bak
 two=p/'b2.kicad_pcb';two.write_text('(kicad_pcb (version 20260101) (layers (0 "F.Cu" signal) (31 "B.Cu" signal)))\n');two.with_suffix('.kicad_dru').write_text(PACK6)
 r=call('rules','install','--profile','jlcpcb','--board',str(two),inrun=False);assert r.returncode==0 and 'jlcpcb-6L' in (r.stdout+r.stderr) and 'replaced' in (r.stdout+r.stderr),(r.stdout,r.stderr)
 r=call('rules','install','--profile','jlcpcb','--board',str(two),'--force',inrun=False);out=r.stdout+r.stderr
 assert r.returncode==0 and 'jlcpcb-2L' in out,out
 assert two.with_suffix('.kicad_dru.bak').read_text()==PACK6
 r=call('rules','check','--profile','jlcpcb','--board',str(two),inrun=False);assert r.returncode==0,(r.stdout,r.stderr)
 # 4. a run whose spec names no fab.rules falls back to run.json fabRules
 assert call('intake','--prompt-time','2026-10-10T00:00:00Z','--engine','test').returncode==0
 spec=p/'spec.json';spec.write_text(json.dumps({}))
 state=run/'run.json';d=json.loads(state.read_text());d['boards']=[str(board)];d['spec']=str(spec)
 d['fabRules']={'profile':'jlcpcb','slug':'jlcpcb','layers':6,'file':'jlcpcb-6L.kicad_dru'};state.write_text(json.dumps(d))
 r=call('rules','check');assert r.returncode==0,(r.stdout,r.stderr)
 o=json.loads(state.read_text())['outcomes']['rules'];assert o['pass'] and o['profile']=='jlcpcb',o
 dru.write_text('(version 1)\n# adom-aiflow rules profile: jlcpcb-2L\n(rule "x")\n')
 r=call('rules','check');assert r.returncode==0 and 'FINDINGS' in r.stdout and '--layers 6' in (r.stdout+r.stderr),(r.stdout,r.stderr)
 # the spec disagreeing with fabRules warns
 spec.write_text(json.dumps({'fab':{'rules':'jlcpcb-2L'}}))
 r=call('rules','check');assert 'fabRules' in r.stderr,(r.stdout,r.stderr)
 print('PASS: install honours --layers and the board, refuses a missing shipped pack, reports a replaced pack; check reads fabRules, the board layer count and warns on disagreement.')