import sys,json,re,csv,hashlib,zipfile,math
from pathlib import Path
ROOT=Path(__file__).resolve().parent;BASE=ROOT.parent;H=BASE/'engineering_handoff_local'
sys.path.insert(0,str(BASE/'parity_metadata_closure_A22'))
from prepare_candidates import run,sha,forms
from align_and_verify import footprint_rows
DST=ROOT/'vacuum_esd_candidate';SRC=BASE/'mosfet_pin_audit_A25/vacuum_corrected';NEW='vacuum_control_evt_a3_esd_railfree';pcb=DST/(NEW+'.kicad_pcb')
c=json.loads((ROOT/'esd_candidate_checks.json').read_text())
assert c['candidate_board_sha256']==sha(pcb) and c['candidate_schematic_sha256']==sha(DST/(NEW+'.kicad_sch'))
d=json.loads((DST/'drc.json').read_text());e=json.loads((DST/'erc.json').read_text())
assert not d['violations'] and not d['unconnected_items'];assert all(not x['violations'] for x in e['sheets'])
assert len(d['schematic_parity'])==4 and all(x['type']=='extra_footprint' for x in d['schematic_parity'])
for folder in ['gerbers','drill']:(DST/folder).mkdir(exist_ok=True)
for args,name in [(['pcb','export','gerbers','--layers','F.Cu,B.Cu,F.Paste,B.Paste,F.Silkscreen,B.Silkscreen,F.Mask,B.Mask,Edge.Cuts','--output',str(DST/'gerbers')+'/',str(pcb)],'gerber-export.log'),(['pcb','export','drill','--format','excellon','--excellon-separate-th','--generate-report','--report-path',str(DST/'drill/drill-report.txt'),'--output',str(DST/'drill')+'/',str(pcb)],'drill-export.log'),(['pcb','export','pos','--format','csv','--units','mm','--side','front','--smd-only','--exclude-dnp','--output',str(DST/'centroid-front.csv'),str(pcb)],'centroid-export.log')]:assert run(args,DST/name)==0
exts={'.gbr','.gtl','.gbl','.gts','.gbs','.gtp','.gbp','.gto','.gbo','.gm1'}
def layer(p):return p.name.split('-',1)[1].rsplit('.',1)[0]
def geom(p):return '\n'.join(x for x in p.read_text().splitlines() if not x.startswith(('%TF.CreationDate,','%TF.ProjectId,','%TO.N,','G04 Created by KiCad')))
og={layer(p):p for p in (SRC/'gerbers').iterdir() if p.suffix in exts};ng={layer(p):p for p in (DST/'gerbers').iterdir() if p.suffix in exts}
assert set(og)==set(ng) and len(ng)==9
changed=[n for n in og if geom(og[n])!=geom(ng[n])]
assert set(changed)=={'F_Cu','B_Cu','F_Mask','F_Paste','F_Silkscreen'},changed
def read(p):
 with p.open(encoding='utf-8-sig',newline='') as f:return list(csv.DictReader(f))
def write(p,rs):
 with p.open('w',encoding='utf-8-sig',newline='') as f:
  w=csv.DictWriter(f,fieldnames=list(rs[0]));w.writeheader();w.writerows(rs)
oldpos={r['Ref']:r for r in read(SRC/'centroid-front.csv')};newpos={r['Ref']:r for r in read(DST/'centroid-front.csv')}
assert set(oldpos)==set(newpos) and len(newpos)==40
assert {r for r in oldpos if oldpos[r]!=newpos[r]}=={'U2'}
s=pcb.read_text();pads={}
for _,_,b,r,_ in footprint_rows(s):
 if r!='U2':continue
 assert '(at 67 13.5)' in b
 for _,_,p in forms(b,'pad'):
  pin=re.match(r'\(pad "([^\"]+)"',p).group(1);net=re.search(r'\(net "([^\"]+)"',p).group(1)
  xy=list(map(float,re.search(r'\(at ([\d.-]+) ([\d.-]+)',p).groups()));size=list(map(float,re.search(r'\(size ([\d.-]+) ([\d.-]+)',p).groups()))
  assert size==[.95,.4] and '(roundrect_rratio 0.125)' in p
  pads[pin]={'xy':xy,'size':size,'net':net}
assert {p:x['net'] for p,x in pads.items()}=={'1':'/USB_DP','2':'/USB_DM','3':'/GND'}
assert [pads[str(i)]['xy'] for i in [1,2,3]]==[[-1.1,-.65],[-1.1,.65],[1.1,0.0]]
def via_positions(p):
 return {tuple(map(float,re.search(r'\(at ([\d.-]+) ([\d.-]+)',x).groups())) for _,_,x in forms(p.read_text(),'via')}
oldv=via_positions(SRC/'vacuum_control_evt_a2_pmos_pinfix.kicad_pcb');newv=via_positions(pcb)
assert oldv-newv=={(48.6867,19.6508),(42.9623,38.1873),(53.3097,19.3474)}
assert newv-oldv=={(67.0,12.85),(68.1,14.5)}
def drill_coords(folder,kind):
 return [x for x in next(folder.glob('*-'+kind+'.drl')).read_text().splitlines() if x.startswith('X')]
assert drill_coords(SRC/'drill','NPTH')==drill_coords(DST/'drill','NPTH')
assert len(drill_coords(SRC/'drill','PTH'))-len(drill_coords(DST/'drill','PTH'))==1
cam={'board_sha256':sha(pcb),'gerber_layers':9,'changed_layers':changed,'unchanged_layers':['B_Mask','B_Paste','B_Silkscreen','Edge_Cuts'],'centroid_total':40,'centroid_changed_only':'U2','centroid_U2':newpos['U2'],'all_other_centroids_unchanged':39,'drill_export':'Fresh; remove 3 old vias and add 2 vias. Old drill file must not be used.','pad_source_dimensions_verified':pads}
cam.update({'removed_via_positions_mm':sorted(oldv-newv),'added_via_positions_mm':sorted(newv-oldv),'PTH_hole_count_delta':-1,'NPTH_coordinates_unchanged':True})
(ROOT/'cam_export_checks.json').write_text(json.dumps(cam,indent=2)+'\n')
c.update({'native_erc_findings':0,'native_drc_violations':0,'native_unconnected_items':0,'remaining_parity':'4 PCB-only mounting-hole warnings','MPN_status':'CONDITIONAL_TOPOLOGY_AND_LAND_CANDIDATE_NOT_AVL','qualified_price':'NONE','remaining_gates':['Actual PD/legacy charging attach, flip, fallback and unplug tests','Interface clamp and ground-return ESD tests on assembled board; datasheet ratings are not board results','Actual motor/fan interface and full product-reference functional equivalence','3.3V gate qualification of drill Q1 and vacuum Q2/Q4','Remaining exact-MPN pins, connectors, complete circuit/layout/thermal/EMC/DFM audit','Complete finished-unit BOM and factory cost quote']})
(ROOT/'esd_candidate_checks.json').write_text(json.dumps(c,indent=2)+'\n')
rows=read(BASE/'power_pin_audit_A26/three_board_candidate_bom_A26.csv')
for r in rows:
 if r['Product']=='vacuum':r['Board']='Vacuum A3 rail-free ESD A27 candidate'
 if r['Product']=='vacuum' and r['Ref']=='U2':
  assert r['CandidateMPN']==''
  r.update({'DesignValue':'TPD2E2U06DCKR','CurrentPCBFootprint':'Vacuum_ESD:TI_DCK0003A_TPD2E2U06','CandidateManufacturer':'Texas Instruments','CandidateMPN':'TPD2E2U06DCKR','SelectionStatus':'CONDITIONAL_ESD_TOPOLOGY_AND_LAND_CANDIDATE_A27','PrimarySourceURL':c['primary_source_url'],'PrimarySourceSHA256':c['primary_source_sha256'],'ChecksPerformed':'SC70-3 DCK pin1 IO1, pin2 IO2, pin3 GND; no VBUS supply pin. Manufacturer land dimensions checked; native ERC/DRC/unconnected 0. Data route reaches protection nodes before PD IC; relocated near connector.','RemainingQualification':'; '.join(c['remaining_gates']),'UnitPriceUSD1000':'','QuoteStatus':'NO_NEW_FACTORY_OR_1K_PRICE_VERIFIED_A27'})
assert len(rows)==110 and sum(bool(r['CandidateMPN']) for r in rows)==86
write(ROOT/'three_board_candidate_bom_A27.csv',rows)
for k in ['drill','vacuum','lamp']:write(ROOT/(k+'_candidate_bom_A27.csv'),[r for r in rows if r['Product']==k])
rfq=read(BASE/'power_pin_audit_A26/three_board_1000_each_RFQ_A26.csv');extra={k:'' for k in rfq[0]}
extra.update({'ManufacturerCandidate':'Texas Instruments','MPN':'TPD2E2U06DCKR','DrillQtyFor1000':'0','VacuumQtyFor1000':'1000','LampQtyFor1000':'0','TotalRFQQty':'1000','Status':'CONDITIONAL_CANDIDATE_NOT_AVL_OR_ORDER'})
rfq.append(extra);assert len(rfq)==38 and sum(int(r['TotalRFQQty']) for r in rfq)==86000
assert not any(r['UnitPriceUSD'] for r in rfq);write(ROOT/'three_board_1000_each_RFQ_A27.csv',rfq)
report=H/'A27-ESD-topology-review.md'
report.write_text('''# A27 吸尘器 PD 输入 ESD 电路修订

状态：FABRICATION_HOLD。本版为工程评审候选；没有下单、加电或整机功能等效结论。

## 完成的电路与 PCB 变更

撤销原 USBLC6-2SC6 将约 5V 保护轨连接到拟议 12V PD 输入的结构，U2 换为 TI TPD2E2U06DCKR 三脚双通道保护候选。1 脚接 USB_DP、2 脚接 USB_DM、3 脚接 GND；该结构没有 VBUS 供电脚。新版原理图中移除旧 VBUS 接线和重复数据引脚，采用专用三脚符号。

封装采用原厂 DCK0003A 推荐焊盘：0.95 × 0.4 mm，左右列中心间距 2.2 mm，同侧引脚间距 0.65 mm，圆角半径 0.05 mm。引脚顶视图与推荐焊盘图已进行图像复核。

U2 从 (51,20) mm 移至 USB-C 接口后方 (67,13.5) mm，改布两条数据线路，使接口引入线路先到保护节点后继续到 PD 控制器。接地回路接回接口 J1 的接地屏蔽焊盘；没有将信号保护电流引到高压 VBUS。拆除旧 VBUS 保护支路和不再使用的局部线路/过孔。器件总数量没有增加。

## 本地验证范围

新鲜 KiCad 10.0.6 ERC 0、物理 DRC 0、未连接 0；原理图与 PCB 一致性只保留 MH1–MH4 四个安装孔提示。U2 三个原厂功能引脚与网表、实际焊盘逐项匹配，焊盘尺寸与圆角逐项复核。其他器件的原理图引脚网络完全相同，其他 PCB 封装块逐字节相同。

重新导出 9 层 Gerber、PTH/NPTH 钻孔和 40 个贴装坐标。仅前后铜、前阻焊、前钢网与前丝印变化；后阻焊、后钢网、后丝印、外形几何不变。贴装坐标只改 U2，其他 39 个记录不变。钻孔文件重新生成：移除 3 个旧过孔，新增 2 个过孔，禁止混用旧钻孔/坐标/钢网文件。

## 成本与打样边界

新 U2 纳入条件候选 BOM，三款候选 MPN 覆盖回到 86/110：电磨 31/39、吸尘器 33/43、灯板 22/28；千台询价表为 38 组、86,000 个已标识位号用量，全部新报价字段为空。这不是 AVL 冻结，也不证明整机成本降低。旧 USBLC6-2SC6 价格不得套用到新 U2。

本轮证明的是 CAD 中已移除确定的电压冲突，不能证明板级 ESD 已通过。仍需实际 PD 与传统充电识别、插头正反、回落、掉电和 ESD 测试，以及 MOSFET 3.3V 驱动、剩余器件引脚/连接器、整机负载/热/EMC/DFM 审核。未获得原产品样机和候选板测量，因此三款功能等效未确认。

原厂依据：https://www.ti.com/lit/ds/symlink/tpd2e2u06.pdf ，SLLSEG9C，第 3 页 DCK 三脚引脚图、第 21 页 DCK0003A 推荐焊盘，已保留 URL 和 PDF SHA256；原厂 PDF 仅本地保存，不在评审 ZIP 中分发。
''',encoding='utf-8')
files=sorted([report]+[p for p in ROOT.rglob('*') if p.is_file() and 'sources' not in p.parts and '__pycache__' not in p.parts])
manifest='\n'.join(sha(p)+'  A27/'+p.relative_to(BASE).as_posix() for p in files)+'\n';zp=H/'Vacuum-rail-free-ESD-CAD-and-BOM-A27.zip'
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z:
 for p in files:z.write(p,'A27/'+p.relative_to(BASE).as_posix())
 z.writestr('A27/SHA256SUMS.txt',manifest)
with zipfile.ZipFile(zp) as z:
 assert z.testzip() is None
 for ln in z.read('A27/SHA256SUMS.txt').decode().splitlines():
  dig,n=ln.split('  ',1);assert hashlib.sha256(z.read(n)).hexdigest()==dig
(H/'A27-SHA256SUMS.txt').write_text(manifest)
idxp=H/'LOCAL-PACKAGES.json';idx=json.loads(idxp.read_text())
entry={'archive':zp.name,'sha256':sha(zp),'bytes':zp.stat().st_size,'zip_entries':len(files)+1,'manifest_entries':len(files),'zip_crc':'PASS','manifest_readback':'PASS','mpn_coverage':'86/110 conditional candidates','native_checks':'ERC 0; DRC 0; unconnected 0; parity 4 mounting holes','physical_validation':'NOT_RUN_NO_SAMPLE','manufacturing_status':'FABRICATION_HOLD'}
idx['packages']=[x for x in idx['packages'] if x.get('archive')!=zp.name]+[entry];idx['status']='A27_VACUUM_ESD_TOPOLOGY_CORRECTED_MPN_86_OF_110_ALL_FABRICATION_HOLD'
idx['latest_report']=report.name;idx['latest_machine_report']='../esd_topology_A27/esd_candidate_checks.json';idx['latest_manifest']='A27-SHA256SUMS.txt'
idx['latest_candidate_sources']['vacuum_control']='../esd_topology_A27/vacuum_esd_candidate/'+NEW+'.kicad_pcb'
idx['latest_bom_candidates']={k:'../esd_topology_A27/'+k+'_candidate_bom_A27.csv' for k in ['drill','vacuum','lamp']}
idx['vacuum_ESD_application_gate']={'status':'DETERMINISTIC_12V_SHUNT_CONFLICT_REMOVED_IN_A27_CAD_PHYSICAL_ESD_NOT_RUN','candidate':'TPD2E2U06DCKR','prior_U2_price_anchor_applicable':False,'powered_test_release':False,'next_gate':'Whole-board circuit audit and controlled powered/PD/ESD bench procedure'}
idx['manufacturing_exclusions']['vacuum_previous_A0_A22']='Wrong AO4407A Q1/Q3 G/S mapping and old 5V-class ESD shunt on planned 12V PD rail; do not manufacture.'
idx['manufacturing_exclusions']['vacuum_A25']='PMOS pinfix valid but ESD conflict remains in A25. A27 supersedes it as review candidate, not manufacturing release.'
idxp.write_text(json.dumps(idx,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'package':entry,'cam':cam},indent=2))
