import sys,json,re,hashlib,zipfile,csv
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 forms,sha,run
from align_and_verify import footprint_rows,netlist
DST=ROOT/'vacuum_corrected';SRC=BASE/'parity_metadata_closure_A22/vacuum';NEW='vacuum_control_evt_a2_pmos_pinfix';OLD='vacuum_control_evt_a1_metadata'
pcb=DST/(NEW+'.kicad_pcb');sch=DST/(NEW+'.kicad_sch')
check=json.loads((ROOT/'pin_fix_checks.json').read_text());assert sha(pcb)==check['candidate_board_sha256']
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 len(d['schematic_parity'])==4 and all(x['type']=='extra_footprint' for x in d['schematic_parity'])
assert all(not sh['violations'] for sh in e['sheets'])
before=(SRC/(OLD+'.kicad_pcb')).read_text();after=pcb.read_text()
oldfp={r:b for _,_,b,r,_ in footprint_rows(before)};newfp={r:b for _,_,b,r,_ in footprint_rows(after)}
assert set(oldfp)==set(newfp)
assert {r for r in oldfp if oldfp[r]!=newfp[r]}=={'Q1','Q3'}
def no_nets(s):return re.sub(r'\(net "[^"]+"\)','(net "<net>")',s)
assert all(no_nets(oldfp[r])==no_nets(newfp[r]) for r in ['Q1','Q3'])
_,pins=netlist(DST/'netlist.xml')
expected={}
for r,src,gate,drain in [('Q1','/VBUS_RAW','/PMOS_GATE','/VBUS_SW'),('Q3','/VBUS_SW','/FAN_FET_GATE','/FAN_SW')]:
 expected[r]={str(p):src if p<4 else gate if p==4 else drain for p in range(1,9)}
 assert all(pins[(r,p)]==n for p,n in expected[r].items())
for folder in ['gerbers','drill']:(DST/folder).mkdir(exist_ok=True)
exports=[(['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')]
for args,name in exports:assert run(args,DST/name)==0
def read(p):
 with p.open(encoding='utf-8-sig',newline='') as f:return list(csv.DictReader(f))
assert read(SRC/'centroid-front.csv')==read(DST/'centroid-front.csv')
def layer(p):return p.name.split('-',1)[1].rsplit('.',1)[0]
def geo(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')))
exts={'.gbr','.gtl','.gbl','.gts','.gbs','.gtp','.gbp','.gto','.gbo','.gm1'}
oldger={layer(p):p for p in (SRC/'gerbers').iterdir() if p.suffix in exts};newger={layer(p):p for p in (DST/'gerbers').iterdir() if p.suffix in exts}
assert len(oldger)==len(newger)==9 and set(oldger)==set(newger)
changed=[k for k in oldger if geo(oldger[k])!=geo(newger[k])]
assert set(changed)=={'F_Cu','B_Cu','F_Mask','B_Mask'},changed
check.update({'candidate_schematic_sha256':sha(sch),'physical_drc_violations':0,'unconnected_items':0,'erc_findings':0,'remaining_parity':'4 extra PCB-only mounting holes','manufacturer_verified_expected_pin_map':expected,'all_other_footprints_byte_identical':True,'Q1_Q3_pad_geometry_unchanged':True,'centroids_unchanged':40,'gerber_layers':9,'changed_gerber_layers':changed,'new_mpn_count':0,'bom_coverage':'86/110 unchanged','primary_source':{'url':'https://www.aosmd.com/res/data_sheets/AO4407A.pdf','sha256':sha(ROOT/'sources/AO4407A.pdf'),'revision':'11.1 March 2024','visually_reviewed_page':1},'open_gates':['Audit every exact-MPN symbol against manufacturer package pinout; schematic-to-PCB agreement alone is insufficient','Drill Q1 and vacuum Q2/Q4 3.3V gate qualification remains open; AO3400A is only a researched alternative, not adopted','Actual handpiece motor and fan interface unknown; architecture equivalence unconfirmed','Reference and prototype physical functional/thermal/EMC tests not run','No full finished-unit cost or factory quote confirmed']})
(ROOT/'pin_fix_checks.json').write_text(json.dumps(check,ensure_ascii=False,indent=2)+'\n')
report=H/'A25-PMOS-pinfix-review.md'
report.write_text('''# A25 吸尘器 AO4407A 引脚修复评审

状态：FABRICATION_HOLD。没有打样下单，没有实机验证，不代表三款功能已走通。

## 修复原因与结果

逐项检查原厂封装时，发现 Q1、Q3 的自定义 PMOS_AO4407A 符号错误：栅极定义为 1 脚，源极定义为 2/3/4 脚。AO4407A 原厂 SOIC-8 顶视图应为 1/2/3 脚源极、4 脚栅极、5/6/7/8 脚漏极。旧版原理图与 PCB 共同使用错误定义，因此 DRC、ERC 和两者一致性通过并不能检出此类原厂引脚错误。

已同步修正嵌入符号、Project 本地符号库、原理图和 PCB。Q1/Q3 仅 1、4 脚网络交换；器件位置、封装焊盘尺寸和其他全部器件保持不变。拆除 6 段旧局部走线、2 个旧过孔，新增 9 段走线、2 个过孔，重新连接源极与栅极。没有通过重编号实体焊盘掩盖引脚错误。

原理图独立导出网表已确认两只 MOSFET 的全部 8 脚与原厂映射相符。新鲜 KiCad 10.0.6 检查：ERC 0、物理 DRC 0、未连接 0；一致性仅保留 MH1–MH4 四个 PCB 安装孔提示。新鲜导出 9 层 Gerber，变化仅前后铜及前后阻焊；钢网、丝印、外形与旧版几何相同，40 个贴装坐标逐项相同。过孔位置发生改变，必须使用本版新钻孔文件。

## 原厂依据

Alpha & Omega AO4407A，Rev.11.1 March 2024，第 1 页封装顶视图，已图像复核：
https://www.aosmd.com/res/data_sheets/AO4407A.pdf
原厂 PDF 仅保留在本地 sources，包内记录 URL 和 SHA256，不重新分发该 PDF。

## 打样与成本边界

本次不增加元件，110 个 BOM 位号中 86 个有候选 MPN 的覆盖数保持不变；不能据此推算整机成本。历史吸尘器 A0/A22 板及包含它们的组合包不得用于制造。A25 是新的待审候选，原板未覆盖删除。

必须继续对关键芯片、MOSFET 和连接器进行原厂引脚核对；不能仅依赖自定义符号内部一致。电磨 Q1、吸尘器 Q2/Q4 的 3.3V 栅极驱动选料仍未闭合：AO3400A 有 2.5V 导通电阻规格，但栅电荷、GPIO 动态驱动、耐压和接口条件未验证，因此本版没有自动替换或增加候选 MPN。

仍无参考样机、电机/风扇接口实测和候选板实机记录，功能等效、热、EMC、整机成本及打样放行均未确认。
''',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)+'  A25/'+p.relative_to(BASE).as_posix() for p in files)+'\n'
zp=H/'Vacuum-PMOS-pinfix-review-A25.zip'
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z:
 for p in files:z.write(p,'A25/'+p.relative_to(BASE).as_posix())
 z.writestr('A25/SHA256SUMS.txt',manifest)
with zipfile.ZipFile(zp) as z:
 assert z.testzip() is None
 for line in z.read('A25/SHA256SUMS.txt').decode().splitlines():
  digest,name=line.split('  ',1);assert hashlib.sha256(z.read(name)).hexdigest()==digest
(H/'A25-SHA256SUMS.txt').write_text(manifest)
idxp=H/'LOCAL-PACKAGES.json';idx=json.loads(idxp.read_text())
entry={'product':'Vacuum AO4407A manufacturer pinout correction A25','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','physical_drc':'0 violations / 0 unconnected','erc':0,'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']='A25_VACUUM_PMOS_PINOUT_CORRECTED_ALL_FABRICATION_HOLD'
idx['latest_report']=report.name;idx['latest_machine_report']='../mosfet_pin_audit_A25/pin_fix_checks.json';idx['latest_manifest']='A25-SHA256SUMS.txt'
idx['latest_candidate_sources']['vacuum_control']='../mosfet_pin_audit_A25/vacuum_corrected/'+NEW+'.kicad_pcb'
idx['manufacturing_exclusions']={'vacuum_previous_A0_A22':'Wrong AO4407A Q1/Q3 G/S physical pin map; historical packages must not manufacture; use A25 as review candidate only','all_candidates':'FABRICATION_HOLD; exact-part pin audit and reference/physical validation incomplete'}
idxp.write_text(json.dumps(idx,ensure_ascii=False,indent=2)+'\n')
print(json.dumps(entry,indent=2))
