from pathlib import Path
import json,csv,hashlib,zipfile,shutil
R=Path(__file__).resolve().parent;B=R.parent;H=B/'engineering_handoff_local';ip=H/'LOCAL-PACKAGES.json';idx=json.loads(ip.read_text())
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
def read(p):
 with p.open(encoding='utf-8-sig',newline='') as f:return list(csv.DictReader(f))
def write(p,rows):
 with p.open('w',encoding='utf-8-sig',newline='') as f:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
boards={k:{'source':str((H/v).resolve()),'sha256':sha((H/v).resolve())} for k,v in idx['latest_candidate_sources'].items()}
measured=read(H/'三款功能等效测量记录模板-A11.csv');samples=read(H/'三款样机登记表-A11.csv');claims=read(H/'产品页面声明到实测验收映射-A10.csv')
measurement_columns=['试次1读数/原始文件','试次2读数/原始文件','试次3读数/原始文件','参考机三次均值','操作者/日期','复核者/日期']
assert len(claims)==20 and len(measured)==80 and len(samples)==12
assert all(not r[k] for r in measured for k in measurement_columns)
assert all(not r[k] for r in samples for k in ['实物序列号/铭牌','实物PCBA版本','实物PCBA SHA-256','实际固件 SHA-256'])
coverage=[]
for i,c in enumerate(claims):
 prod=c['产品']
 if prod=='电磨':stage='A34 motor + A32 controller + A33 hard-off image; runtime motor/ADC/UI/charge integration unproven';gate='Actual motor/battery + full system runtime, loaded RPM and battery test'
 elif prod=='吸尘器':stage='A29 control + A30 diagnostic image; fan locked off';gate='Actual supply/fan/profile + running capture/noise/filter/thermal comparison'
 else:stage='A24 power + existing guard/UI/AI transport candidates; A35 independent IR policy host only';gate='Physical IR/AI/camera/display/61LED driver + optics/calibration/current/ramp integration'
 coverage.append({'Requirement':str(i+1),'Product':prod,'ASIN':c['子ASIN'],'Claim':c['页面功能声明（转述）'],'ImplementationEvidenceScope':stage,'RequiredEvidence':c['样机方法与记录'],'AcceptanceCriteria':c['预设通过判据'],'CurrentPhysicalEvidence':'A11 records blank; no PASS found in current register','Status':'NOT_VERIFIED','NextGate':gate})
write(R/'whole_product_20_requirements_A35.csv',coverage)
# Refresh proposed candidate board identities; preserve all actual measurement fields blank.
for s in samples:
 if s['角色']=='参考机':continue
 prod=s['产品']
 keys=['drill_motor','drill_control'] if prod=='电磨' else ['vacuum_control'] if prod=='吸尘器' else ['lamp_power']
 names={'drill_motor':'A34 motor','drill_control':'A32 PF1 controller','vacuum_control':'A29 control','lamp_power':'A24 power only'}
 s['计划候选板版本']='; '.join(names[k] for k in keys)
 s['计划候选PCBA SHA-256']='; '.join(names[k]+':'+boards[k]['sha256'] for k in keys)
 s['计划来源范围']='PARTIAL BOARD SET ONLY; not complete product; manufacturing hold'
 s['计划固件范围']='A33 hard-off' if prod=='电磨' else 'A30 diagnostic fan-off' if prod=='吸尘器' else 'Existing hard-off target only; A35 IR policy NOT linked into image'
 s['状态']='NOT_RECEIVED_OR_NOT_REGISTERED; FABRICATION_HOLD'
for s in samples:
 s.setdefault('计划来源范围','REFERENCE TO ACQUIRE');s.setdefault('计划固件范围','REFERENCE FIRMWARE UNKNOWN')
# Normalize column order across rows before writing.
cols=list(samples[0]);samples=[{k:s[k] for k in cols} for s in samples];write(R/'current_sample_board_plan_A35.csv',samples)
checks={'status':'A35_LAMP_IR_POLICY_HOST_PASS_FULL_PRODUCT_NOT_VERIFIED','scope':'Portable guard policy only. No CAD change, GPIO binding, sensor circuit, output HAL link or target flash image generated','baseline':json.loads((R/'baseline.json').read_text()),'guard_sha256':sha(R/'lamp_control/lamp_guard.c'),'header_sha256':sha(R/'lamp_control/lamp_guard.h'),'tests':'Original guard33 zero-output invariants + program44 checks + AI/manual IR removal/reentry/late packet/timer/rollover/omitted-input scenarios; ASan/UBSan PASS','negative_control':'Original guard fails the exact IR-absent ticket assertion','target_objects':{n:sha(R/(n+'.o')) for n in ['lamp_guard','lamp_program']},'target_link':'NOT_RUN_NOT_A_FLASH_IMAGE','ABI_change':'lg_inputs gains infrared_hand_present; all callers must rebuild and supply verified physical/debounced IR. Existing omitted field defaults false, so cannot grant light. Test fixture good explicitly sets IR true; no target input is fabricated.','semantics':'IR absence clears PWM, cancels pending ticket and vision status; normal absence does not fault or reset session time; reentry needs new vision ticket/result and manual command for manual mode. Arming while absent remains permitted, preserving existing timer semantics.','physical_binding':'UNIMPLEMENTED; no trusted IR GPIO/electrical polarity or measured latency','board_candidates_verified_now':boards,'requirement_count':len(claims),'verified_physical_requirements':0,'measurement_rows_checked':len(measured),'measurement_value_fields':'ALL_BLANK','registered_physical_samples':0,'sample_plan_refresh':'A11 historical register proposed old board hashes. A35 refreshes partial board identities; physical hashes/serials remain blank.','manufacturing_status':'FABRICATION_HOLD','physical_validation':'NOT_RUN_NO_SAMPLE'}
(R/'checks.json').write_text(json.dumps(checks,ensure_ascii=False,indent=2)+'\n')
report=H/'A35-lamp-IR-and-product-coverage.md';report.write_text('''# A35 AI灯红外策略与三款整机覆盖核验

状态：可编辑控制策略主机测试通过；不是可烧录整灯固件。FABRICATION_HOLD，原产品功能等效未验证。

## 新增红外门控策略

现有lg_inputs只有温度、供电、驱动和硬件联锁，hand_present来自AI_RESULT，没有独立红外输入。A35增加infrared_hand_present，要求调用者提供经过实际传感器极性/断线与去抖验证的物理信号，禁止从AI_RESULT复制生成。没有假设传感器型号、电平或GPIO。

红外检测无手/移手时，lg_poll清零PWM，取消待返回票据与视觉存在许可。晚到识别包不能恢复灯光。重新入手不自动恢复旧PWM；AI需新的识别结果，手动模式还需重新发出手动亮度命令。无手是正常停止条件，不锁定故障、不重置倒计时。沿用原策略允许无手时先arm，不宣称参考机UI行为已确认。

复测原guard的33项清零不变量与程序44项检查；新增AI/手动、移手、重新入手、晚包、到期、时钟回绕及遗漏IR字段的测试，ASan/UBSan通过。旧guard在同一个红外无手断言失败。Cortex-M0+对象编译通过，但没有目标链接、烧录或实际输出测量。

ABI已变化：所有调用lg_inputs的源必须重新编译并明确接入真实IR。仅修改测试fixture的正常输入为IR=true，未给实际目标伪造IR信号。本候选只交付策略与测试，未替换现有灯固件镜像。红外传感器电路、引脚绑定、采样/去抖、硬件门控、目标HAL与关断延迟仍未完成，不能靠主机测试认定红外功能已落地。

## 三款完整范围仍未通过

本轮逐条对照A10的20项要求，并检查A11的80行测量与12行样机登记。测量读数/操作者等字段均空，实物序列号与实际板/固件哈希均空；当前记录中可验收的物理功能通过项为0/20。详见whole_product_20_requirements_A35.csv。它不是测试结果，也不允许把部分CAD/模块检查换算为整机完成百分比。

电磨仍需真实电机、电池、充电/UI/ADC/FOC与保护链运行整合及50000rpm、续航、振动/噪声比较。吸尘器仍需确认风机/电源并接通运行、测速、故障关断，再验证粉尘捕集/滤网/噪声。AI灯仍需IR、实际摄像头与AI模型、61LED和双波段光学、电流与渐进曲线、显示/定时及全部轨路的整机集成和实测。

A11的计划样机登记还指向旧板。A35已更新计划来源为A34电机板/A32控制板、A29吸尘板、A24灯电源板，逐项记录当前真实SHA256。此处只列部分板卡集合，不能当作三款完整整机物料或制造包。A35红外策略未链接入灯镜像，不能交给工厂当作已可运行固件。所有实物字段继续留空，没有下单、制造或上电。

三款各1000台低成本目标保持不变；完整BOM、结构/负载器件、装配测试和工厂报价仍未闭合。下一步优先完成实际目标硬件与运行路径的缺口，最终用参考机/候选机同台比较证明功能效果。
''',encoding='utf-8')
files=[report]+[p for p in R.rglob('*') if p.is_file() and p.suffix not in ['.o'] and p.name not in ['test_lamp_guard','test_lamp_program','test_lamp_ir','original_negative'] and '__pycache__' not in p.parts]
manifest=''.join(sha(p)+'  A35/'+p.relative_to(B).as_posix()+'\n' for p in sorted(files));zp=H/'Lamp-IR-policy-and-product-coverage-A35.zip'
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z:
 for p in sorted(files):z.write(p,'A35/'+p.relative_to(B).as_posix())
 z.writestr('A35/SHA256SUMS.txt',manifest)
with zipfile.ZipFile(zp) as z:
 assert z.testzip() is None
 for ln in z.read('A35/SHA256SUMS.txt').decode().splitlines():dig,n=ln.split('  ',1);assert hashlib.sha256(z.read(n)).hexdigest()==dig
(H/'A35-SHA256SUMS.txt').write_text(manifest);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','host_tests':'PASS ASan/UBSan; original negative control failed as expected','target_compile':'Cortex-M0+ OBJECT_PASS_NOT_LINKED','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.update({'status':'A35_LAMP_IR_POLICY_CANDIDATE_0_OF_20_PHYSICAL_VERIFIED_FABRICATION_HOLD','latest_report':report.name,'latest_machine_report':'../lamp_ir_A35/checks.json','latest_manifest':'A35-SHA256SUMS.txt'});idx['latest_lamp_IR_policy_A35']={'source':'../lamp_ir_A35/lamp_control','target_linked':False,'IR_GPIO_binding':'UNIMPLEMENTED','guard_sha256':checks['guard_sha256'],'physical_test_release':False};idx['current_whole_product_coverage_A35']={'csv':'../lamp_ir_A35/whole_product_20_requirements_A35.csv','requirements':20,'physical_verified':0,'sample_plan':'../lamp_ir_A35/current_sample_board_plan_A35.csv','scope':'Current recorded physical evidence audit, not completeness percentage'};ip.write_text(json.dumps(idx,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'package':entry,'physical_verified':'0/20 in current records','sample_plan':'12 planned slots; actual serials/hashes blank'},ensure_ascii=False,indent=2))
