            byid={r['id']:r for r in data['contacts']};merged=[]
            for ids in data['approved_groups']:
                if ids not in output['candidate_groups']:raise ValueError('模型未提出批准组')
                emails={byid[x]['email'] for x in ids};check('合并组相同邮箱',len(emails)==1);merged.append({'canonical_id':ids[0],'source_ids':ids,'email':byid[ids[0]]['email']})
            check('同名不同邮箱仍单列','C2' not in {i for r in merged for i in r['source_ids']});jout('merge-preview.json',merged);cout('contacts-unchanged.csv',data['contacts'])
            with (folder/'contacts-unchanged.csv').open(encoding='utf-8-sig') as f:back=list(csv.DictReader(f))
            check('原始联系人CSV回读保留全部ID',[r['id'] for r in back]==['C1','C2','C3']);result={'approved_preview':merged,'unmerged':['C2'],'written_to_service':False}
