from pathlib import Path
import json,hashlib,zipfile,csv,math
R=Path(__file__).resolve().parent;B=R.parent;H=B/'engineering_handoff_local';sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
metadata=json.loads((R/'metadata_checks.json').read_text());diff=json.loads((R/'IR_circuit_diff.json').read_text())
contract={'status':'CANDIDATE_INTERFACE_CONTRACT_NOT_VALIDATED_SENSOR','connector':'J6 1 CTRL_3V3 / 2 GND / 3 IR_SENSOR','signal':'active-high 3.3V-domain push-pull; absent=LOW; floating/open defaults LOW','R20_series_ohm':1000,'R21_pulldown_ohm':47000,'C8_node_to_GND_F':10e-9,'R_tolerance_percent':1,'nominal_high_divider_ratio':47000/48000,'worst_high_divider_ratio_1pct_R':46530/(46530+1010),'assumed_sensor_min_VOH_fraction_VDD':0.9,'calculated_min_input_high_fraction_VDD':.9*46530/(46530+1010),'nominal_open_sensor_discharge_tau_ms':47000*10e-9*1000,'nominal_open_sensor_decay_to_0p3VDD_ms':-47000*10e-9*1000*math.log(.3),'caveat':'Static assumptions only, not VOH/VIL qualification. Exact sensor not selected; power budget, optical placement, gloves/skin/ambient-light/crosstalk behavior, voltage tolerance, leakage, cable/ESD and MCU RTC mux need verification. No 5V input, active-low/open-drain sensor compatibility or optical detection guarantee.','firmware_candidate_policy':'IR presence assertion requires continuous high/debounce; removal immediately revokes permission. Debounce value requires physical response tests.','PC15_constraint':'DS12991 Rev6 Table12 note2: GPIO depends on RTC domain configuration retained across system reset. Do not bind input until RTC/mux ownership and LSE state are handled.','physical_test_release':False}
(R/'IR_interface_contract.json').write_text(json.dumps(contract,ensure_ascii=False,indent=2)+'\n')
with (R/'IR_added_placements_BOM.csv').open('w',newline='') as f:
 w=csv.writer(f);w.writerow(['ref','candidate_value','footprint','quantity_per_board','exact_MPN','unit_price','qualification'])
 for ref,value,fp in [('J6','3pin 2.54mm internal IR connector','PinHeader_1x03_P2.54mm_Vertical'),('R20','1k 1%','R_0603_1608Metric'),('R21','47k 1%','R_0603_1608Metric'),('C8','10n X7R 16V','C_0603_1608Metric')]:w.writerow([ref,value,fp,1,'','','CANDIDATE_NOT_QUALIFIED'])
report=f'''# 灯具安全板元数据闭合与红外接口原理图 A38

状态：原有安全板原理图/PCB元数据已本地闭合；新红外接口仅为原理图候选。FABRICATION_HOLD；20项整机验收仍0项实测完成。

## 原有安全板本轮核验

从本地真实字节建立 baseline 并运行 KiCad10 原生 ERC、带原理图一致性和区域重填充的 DRC：ERC0、物理DRC0、未连线0，但有154项parity（120 net_conflict + 34 footprint_symbol_mismatch）。原因是 PCB 网名缺原理图根路径、封装名缺库前缀、NC标注缺失。

`paired/lamp_safety_evt_a1_metadata.*` 只修改上述元数据和原理图项目实例名，原生保存前的铜皮、焊盘、位置、尺寸等几何token与 baseline 一致。120个参考号/物理脚网名逐项核对。复测 ERC0、DRC0、未连线0、parity0。板 SHA `{metadata['candidate_board_sha256']}`。这些检查沿用项目标准检查设置；ignored_checks 保留在报告JSON中，未关闭新检查来消除差异。没有修改物理电路，也未实测。

## 红外接口原理图候选

`IR_schematic_candidate/lamp_safety_evt_a2_IR_interface.kicad_sch` 新增 J6三针（1=CTRL_3V3、2=GND、3=IR_SENSOR）、R20串联1k1%、R21节点下拉47k1%、C8节点对地10n X7R16V。IR_HAND接U1第3脚PC15。新ERC为0；129个网表脚位相对于120脚旧版，仅U1.3由NC变成IR_HAND，并新增9个器件脚连接，其他旧脚位网名不变。原理图画布扩大至A2便于新增电路排放，不改变PCB板框。

协议是3.3V域主动高推挽“有手”候选，未接/断线通过下拉默认无手。它不是已选定或已验证的红外传感器，不能承诺识别距离、肤色/手套、环境光抗干扰或与原产品相同的自动感应效果。不能接5V、不能直接混用主动低/开漏模块。传感器供电负载、电缆、ESD和光学安装均未闭合。元件4个新增位置的MPN及价格保持空白，千台整机成本尚无完整报价。

原厂依据：[ST DS12991 Rev6 Table12 note2](https://www.st.com/resource/en/datasheet/stm32g030f6.pdf)：PC15受RTC域配置影响且配置在系统复位后保留，需处理时钟和复用归属后才能接入固件。接口RC/分压计算见 `IR_interface_contract.json`，仅静态假设，不是输入门限或实际传感器资格证明。

## 禁止混版与剩余工作

paired 是旧电路的元数据修订，PC15仍NC；IR_schematic_candidate 是另一版电路。**本轮没有为新增红外电路建立PCB，没有IR CAM或打样放行。不能把旧 paired PCB与新增红外原理图混合下单。** A37仍是当前灯具目标固件，物理IR未绑定；温度/供电ADC、驱动反馈与输出闭环还未完成。本板为PCA9956B安全控制分支，IS31FL3236A使能极性不同，不能混用。

下一步在IR电路上完成PCB布局布线和PC15/RTC输入初始化，再完成传感器选型及实际样机光学/电气验证。三款整机结构、负载匹配、温升、真实效果及整机千台成本仍待闭合。没有采购、供应商联系、烧录或通电。
'''
(R/'README.md').write_text(report);(H/'A38-lamp-safety-parity-and-IR-schematic.md').write_text(report)
checks={'status':'A38_SAFETY_METADATA_CLOSED_IR_SCHEMATIC_ONLY_FABRICATION_HOLD','metadata':metadata,'IR_circuit_diff':diff,'IR_interface_contract':contract,'physical_verified_requirements':0,'reference_requirements':20,'physical_validation':'NOT_RUN_NO_SAMPLE'}
(R/'checks.json').write_text(json.dumps(checks,ensure_ascii=False,indent=2)+'\n')
files=[p for p in R.rglob('*') if p.is_file() and p.name not in ['inspect.py','baseline.py','close_metadata.py','add_IR_schematic.py','package.py'] and not p.name.startswith('.') and p.suffix!='.kicad_prl']
manifest=''.join(f'{sha(p)}  {p.relative_to(R).as_posix()}\n' for p in sorted(files));(H/'A38-SHA256SUMS.txt').write_text(manifest)
z=H/'Lamp-safety-parity-and-IR-schematic-review-A38.zip'
with zipfile.ZipFile(z,'w',zipfile.ZIP_DEFLATED) as a:
 for p in files:a.write(p,p.relative_to(R).as_posix())
 a.writestr('SHA256SUMS.txt',manifest)
with zipfile.ZipFile(z) as a:
 assert a.testzip() is None
 for line in manifest.splitlines():
  digest,name=line.split('  ',1);assert hashlib.sha256(a.read(name)).hexdigest()==digest
 count=len(a.namelist())
j=json.loads((H/'LOCAL-PACKAGES.json').read_text());j['status']=checks['status'];j['latest_report']='A38-lamp-safety-parity-and-IR-schematic.md';j['latest_machine_report']='../lamp_IR_hardware_A38/checks.json';j['latest_manifest']='A38-SHA256SUMS.txt'
j['packages'].append({'archive':z.name,'sha256':sha(z),'bytes':z.stat().st_size,'zip_entries':count,'manifest_entries':len(files),'zip_crc':'PASS','manifest_readback':'PASS','manufacturing_status':'FABRICATION_HOLD'})
j['latest_candidate_sources']['lamp_safety_PCA_metadata']='../lamp_IR_hardware_A38/paired/lamp_safety_evt_a1_metadata.kicad_pcb'
j['latest_lamp_IR_hardware_A38']={'source':'../lamp_IR_hardware_A38/IR_schematic_candidate/lamp_safety_evt_a2_IR_interface.kicad_sch','ERC_findings':0,'PCB_created':False,'IR_firmware_bound':False,'physical_test_release':False,'pairing':'Schematic-only new circuit; do not pair with A38 metadata PCB'}
j['manufacturing_exclusions']['lamp_A38_IR_schematic_pairing']='IR schematic changes PC15 and adds4placements; A38 metadata PCB retains PC15 NC. Mixing prohibited; no IR PCB/CAM yet.'
(H/'LOCAL-PACKAGES.json').write_text(json.dumps(j,ensure_ascii=False,indent=2)+'\n')
print(json.dumps(j['packages'][-1],indent=2))
