#!/usr/bin/env python3
"""Create an isolated LMR51450 lamp-buck schematic candidate from the local A1 source.

This intentionally produces schematic/netlist evidence only. It does not copy or
modify a PCB and does not create fabrication data.
"""
from __future__ import annotations

import hashlib
import json
import re
import shutil
import subprocess
import uuid
import xml.etree.ElementTree as ET
from pathlib import Path

WORK = Path(__file__).resolve().parent
SOURCE = WORK / "compact_bench_suite/lamp_power/lamp_power.kicad_sch"
OUT = WORK / "lamp_buck_lmr51450_a1"
SCH = OUT / "lamp_buck_lmr51450_a1.kicad_sch"
FP_DIR = OUT / "Lamp_Power.pretty"
FP = FP_DIR / "LMR51450_WSON_DRR0012.kicad_mod"
BOARD_SOURCE = SOURCE.parent / "lamp_power_evt_a1_compact_bench.kicad_pcb"
KICAD_CLI = Path("/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli")


def blocks(text: str, token: str):
    pos = 0
    while True:
        start = text.find(token, pos)
        if start < 0:
            return
        depth = 0
        quoted = False
        escaped = False
        for i in range(start, len(text)):
            ch = text[i]
            if quoted:
                if escaped:
                    escaped = False
                elif ch == "\\":
                    escaped = True
                elif ch == '"':
                    quoted = False
            else:
                if ch == '"':
                    quoted = True
                elif ch == "(":
                    depth += 1
                elif ch == ")":
                    depth -= 1
                    if depth == 0:
                        yield start, i + 1, text[start:i + 1]
                        pos = i + 1
                        break
        else:
            raise RuntimeError(f"unbalanced S-expression at {token}")


def make_pin(kind: str, name: str, number: int, x: float, y: float, angle: int) -> str:
    return (
        f'(pin {kind} line (at {x:g} {y:g} {angle}) (length 5.08) '
        f'(name "{name}" (effects (font (size 1 1)))) '
        f'(number "{number}" (effects (font (size 1 1)))))'
    )


def lmr_symbol() -> str:
    # Keep the four original signal-pin anchors where possible so existing
    # converter-control wiring stays legible; expose every parallel power pin.
    pins = [
        make_pin("passive", "BOOT", 4, -12.7, 5.08, 0),
        make_pin("power_in", "VIN", 10, -12.7, 2.54, 0),
        make_pin("input", "EN", 9, -12.7, 0, 0),
        make_pin("input", "RT", 6, -12.7, -2.54, 0),
        make_pin("power_in", "VIN", 11, -12.7, 7.62, 0),
        make_pin("power_in", "VIN", 12, -12.7, 10.16, 0),
        make_pin("passive", "SW", 1, 12.7, 5.08, 180),
        make_pin("passive", "SW", 2, 12.7, 2.54, 180),
        make_pin("passive", "SW", 3, 12.7, 0, 180),
        make_pin("input", "FB", 7, 12.7, -2.54, 180),
        make_pin("open_collector", "PG", 5, 12.7, -5.08, 180),
        make_pin("power_in", "PGND", 13, 0, -10.16, 90),
        make_pin("power_in", "AGND", 8, 5.08, -10.16, 90),
    ]
    return (
        '(symbol "Project:LMR51450" (pin_names (offset 0.8)) (in_bom yes) (on_board yes) '
        '(property "Reference" "U" (at 0 0 0) (effects (font (size 1 1)))) '
        '(property "Value" "LMR51450SDRRR" (at 0 0 0) (effects (font (size 1 1)))) '
        '(property "Footprint" "Lamp_Power:LMR51450_WSON_DRR0012" (at 0 0 0) (effects (font (size 1 1)))) '
        '(property "Datasheet" "https://www.ti.com/lit/ds/symlink/lmr51450.pdf" (at 0 0 0) '
        '(effects (font (size 1 1)) (hide yes))) '
        '(property "Description" "36-V 5-A synchronous buck, WSON-12 DRR" (at 0 0 0) '
        '(effects (font (size 1 1)) (hide yes))) '
        '(symbol "LMR51450_0_1" (rectangle (start -10.16 12.7) (end 10.16 -7.62) '
        '(stroke (width 0.254) (type default)) (fill (type background)))) '
        '(symbol "LMR51450_1_1" ' + "".join(pins) + '))'
    )


def wire_label(x1: float, y: float, x2: float, name: str) -> str:
    return (
        f'(wire (pts (xy {x1:g} {y:g}) (xy {x2:g} {y:g})) '
        f'(stroke (width 0) (type default)) (uuid "{uuid.uuid4()}"))'
        f'(label "{name}" (at {x2:g} {y:g} 0) '
        f'(effects (font (size 1 1)) (justify left bottom)) (uuid "{uuid.uuid4()}"))'
    )


def patch_instance(text: str, ref: str, value: str | None = None,
                   footprint: str | None = None, dnp: bool | None = None) -> str:
    for start, end, body in blocks(text, '(symbol (lib_id '):
        if f'(property "Reference" "{ref}"' not in body:
            continue
        if value is not None:
            body = re.sub(r'\(property "Value" "(?:[^"\\]|\\.)*"',
                          lambda m: m.group(0).split('"', 3)[0] + '"Value" "' + value + '"',
                          body, count=1)
        if footprint is not None:
            body = re.sub(r'\(property "Footprint" "(?:[^"\\]|\\.)*"',
                          lambda m: m.group(0).split('"', 3)[0] + '"Footprint" "' + footprint + '"',
                          body, count=1)
        if dnp is not None:
            body = body.replace('(dnp no)', '(dnp yes)' if dnp else '(dnp no)', 1)
        return text[:start] + body + text[end:]
    raise RuntimeError(f"schematic instance not found: {ref}")


def patch_schematic() -> None:
    text = SOURCE.read_text(encoding="utf-8")
    old_lib = next((b for _, _, b in blocks(text, '(symbol "Project:TPS54540"')), None)
    if old_lib is None:
        raise RuntimeError("TPS54540 embedded library symbol missing")
    text = text.replace(old_lib, lmr_symbol(), 1)
    text = text.replace('(lib_id "Project:TPS54540")', '(lib_id "Project:LMR51450")', 1)
    text = text.replace('(title "', '(title "AI lamp power LMR51450 EVT candidate A1 - ', 1)
    text = text.replace('RT_400KHZ', 'RT_OPEN_500KHZ')
    text = patch_instance(text, "U1", "LMR51450SDRRR 12/15 V TO 5.0 V synchronous buck candidate",
                          "Lamp_Power:LMR51450_WSON_DRR0012")
    text = patch_instance(text, "L1", "4.7uH; Isat >= 10A; RMS >= 6A target")
    text = patch_instance(text, "R3", "100k 1% LMR51450 FB high side")
    text = patch_instance(text, "R4", "19.1k 1% LMR51450 FB low side")
    text = patch_instance(text, "C1", "4.7uF 50V X7R input candidate")
    text = patch_instance(text, "C2", "4.7uF 50V X7R input candidate")
    text = patch_instance(text, "C3", "100nF 50V X7R high-frequency input bypass",
                          "Capacitor_SMD:C_0603_1608Metric")
    text = patch_instance(text, "R1", "DNP - RT open, nominal 500kHz", dnp=True)
    text = patch_instance(text, "D1", "DNP - synchronous rectification integrated", dnp=True)
    text = patch_instance(text, "R5", "DNP - old external compensation removed", dnp=True)
    text = patch_instance(text, "C9", "DNP - old external compensation removed", dnp=True)
    text = patch_instance(text, "C10", "DNP - old external compensation removed", dnp=True)

    # Add the other two VIN pins. The three SW pins reuse the old SW/GND/COMP
    # schematic wire anchors, whose labels are corrected below.
    extra = (
        wire_label(152.4, 104.14, 144.78, "VIN_PD_12_15")
        + wire_label(152.4, 101.6, 144.78, "VIN_PD_12_15")
        + f'(no_connect (at 177.8 116.84) (uuid "{uuid.uuid4()}"))'
    )
    for start, end, body in blocks(text, '(label '):
        if '(label "GND" (at 185.42 109.22 ' in body:
            text = text[:start] + body.replace('(label "GND"', '(label "SW"', 1) + text[end:]
            break
    for start, end, body in blocks(text, '(label '):
        if '(label "COMP" (at 185.42 111.76 ' in body:
            text = text[:start] + body.replace('(label "COMP"', '(label "SW"', 1) + text[end:]
            break

    # Split the PGND wire at the AGND pin location so both ground pins land on
    # explicit schematic junction endpoints.
    replaced_ground = False
    for start, end, body in blocks(text, '(wire (pts '):
        coords = re.findall(r'\(xy ([\d.]+) ([\d.]+)\)', body)
        if coords == [('165.1', '121.92'), ('172.72', '121.92')]:
            split = (
                f'(wire (pts (xy 165.1 121.92) (xy 170.18 121.92)) '
                f'(stroke (width 0) (type default)) (uuid "{uuid.uuid4()}"))'
                f'(wire (pts (xy 170.18 121.92) (xy 172.72 121.92)) '
                f'(stroke (width 0) (type default)) (uuid "{uuid.uuid4()}"))'
            )
            text = text[:start] + split + text[end:]
            replaced_ground = True
            break
    if not replaced_ground:
        raise RuntimeError("converter ground wire for AGND split not found")

    text = text.rstrip()
    if not text.endswith(")"):
        raise RuntimeError("schematic root close missing")
    text = text[:-1] + extra + ")\n"
    text = text.replace(
        "TPS54540 EN uses VIN_PD_12_15 UVLO divider R11/R12; 5V buck starts independently of the safety MCU.",
        "LMR51450 EN uses the same R11/R12 UVLO divider; typ rising threshold is about 9.76V.",
    )
    SCH.write_text(text, encoding="utf-8")


def make_footprint() -> None:
    FP_DIR.mkdir(parents=True, exist_ok=True)
    lines = [
        '(footprint "LMR51450_WSON_DRR0012" (version 20240108) (generator "pcbnew")',
        '  (layer "F.Cu")',
        '  (descr "TI DRR0012G WSON 3x3mm, 0.5mm pitch; exposed pad 1.3x2.5mm; TI datasheet land pattern")',
        '  (tags "LMR51450 WSON DRR0012 12pin 3x3 0.5mm")',
        '  (attr smd)',
        '  (fp_rect (start -1.55 -1.55) (end 1.55 1.55) (stroke (width 0.05) (type default)) (fill none) (layer "F.CrtYd") (uuid "' + str(uuid.uuid4()) + '"))',
        '  (fp_rect (start -1.5 -1.5) (end 1.5 1.5) (stroke (width 0.05) (type default)) (fill none) (layer "F.Fab") (uuid "' + str(uuid.uuid4()) + '"))',
        '  (fp_poly (pts (xy -1.35 -1.35) (xy -1.05 -1.35) (xy -1.35 -1.05)) (stroke (width 0.05) (type default)) (fill solid) (layer "F.SilkS") (uuid "' + str(uuid.uuid4()) + '"))',
        '  (fp_text reference "REF**" (at 0 -2.1) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.12))) (uuid "' + str(uuid.uuid4()) + '"))',
        '  (fp_text value "LMR51450_WSON_DRR0012" (at 0 2.1) (layer "F.Fab") (effects (font (size 0.7 0.7) (thickness 0.1))) (uuid "' + str(uuid.uuid4()) + '"))',
    ]
    # TI DRR0012G: six 0.5-mm-pitch pads on each long side, 0.62 x 0.25 mm.
    for index in range(6):
        y = -1.25 + 0.5 * index
        padno = index + 1
        lines.append(f'  (pad "{padno}" smd rect (at -1.25 {y:g}) (size 0.62 0.25) (layers "F.Cu" "F.Mask" "F.Paste") (uuid "{uuid.uuid4()}"))')
    for index in range(6):
        y = 1.25 - 0.5 * index
        padno = index + 7
        lines.append(f'  (pad "{padno}" smd rect (at 1.25 {y:g}) (size 0.62 0.25) (layers "F.Cu" "F.Mask" "F.Paste") (uuid "{uuid.uuid4()}"))')
    # Exposed pad copper/mask with three controlled paste windows. Thermal vias
    # are deliberately a PCB placement decision and are not silently built in.
    lines.append(f'  (pad "13" smd rect (at 0 0) (size 1.3 2.5) (layers "F.Cu" "F.Mask") (uuid "{uuid.uuid4()}"))')
    for y0, y1 in [(-1.12, -0.40), (-0.36, 0.36), (0.40, 1.12)]:
        lines.append(
            f'  (fp_rect (start -0.55 {y0:g}) (end 0.55 {y1:g}) '
            f'(stroke (width 0) (type solid)) (fill solid) (layer "F.Paste") (uuid "{uuid.uuid4()}"))'
        )
    lines.append(')')
    FP.write_text("\n".join(lines) + "\n", encoding="utf-8")
    (OUT / "fp-lib-table").write_text(
        '(fp_lib_table\n  (lib (name "Lamp_Power") (type "KiCad") '
        '(uri "${KIPRJMOD}/Lamp_Power.pretty") (options "") '
        '(descr "Lamp power EVT candidate footprints"))\n)\n',
        encoding="utf-8",
    )


def create_external_libraries() -> None:
    sch_text = SCH.read_text(encoding="utf-8")
    embedded = next((body for _, _, body in blocks(sch_text, "(lib_symbols ")), None)
    if embedded is None:
        raise RuntimeError("schematic embedded symbol library missing")
    symbols = []
    for _, _, body in blocks(embedded, '(symbol "Project:'):
        symbols.append(body.replace('(symbol "Project:', '(symbol "', 1))
    if not symbols:
        raise RuntimeError("no Project symbols found in candidate schematic")
    (OUT / "Project.kicad_sym").write_text(
        '(kicad_symbol_lib (version 20231120) (generator "kicad_symbol_editor")\n'
        + "\n".join(symbols) + "\n)\n", encoding="utf-8")
    (OUT / "sym-lib-table").write_text(
        '(sym_lib_table\n  (lib (name "Project") (type "KiCad") '
        '(uri "${KIPRJMOD}/Project.kicad_sym") (options "") '
        '(descr "Local lamp power candidate symbols"))\n)\n',
        encoding="utf-8")

    # Preserve the exact legacy lamp-switch footprint referenced elsewhere in
    # the copied lamp power sheet, so the new project resolves both local parts.
    board_text = BOARD_SOURCE.read_text(encoding="utf-8")
    u3 = next((body for _, _, body in blocks(board_text, '(footprint "')
               if '(property "Reference" "U3"' in body), None)
    if u3 is None:
        raise RuntimeError("source board U3 footprint not found")
    libname = re.search(r'\(footprint "([^"]+)"', u3).group(1)
    u3 = re.sub(r'\(at [^()]+\)', '(at 0 0 0)', u3, count=1)
    u3 = u3.replace('(property "Reference" "U3"', '(property "Reference" "REF**"', 1)
    u3 = re.sub(r'\(property "Value" "(?:[^"\\]|\\.)*"',
                f'(property "Value" "{libname}"', u3, count=1)
    u3 = re.sub(r'\(net (?:\d+ )?"[^"\\]*"\)', '', u3)
    (FP_DIR / f"{libname}.kicad_mod").write_text(u3 + "\n", encoding="utf-8")


def write_candidate_docs(result: dict) -> None:
    (OUT / "BOM-delta-1000pcs.csv").write_text(
        """line,baseline,candidate,baseline_public_catalog_USD,candidate_public_catalog_USD,delta_USD_per_unit,status
Buck IC,TPS54540DDAR C95286,LMR51450SDRRR C6890985,0.8079,0.6076,0.2003,2026-10-10 catalog snapshot only
External catch diode,PDS760-13 C96113,DNP synchronous rectification integrated,0.6337,0.0000,0.6337,PDS760 page reported out of stock; reference value only
Inductor,5.5uH Isat>=8A RMS>=5A,4.7uH Isat>=10A RMS>=6A target,,,TBD,exact MPN and current public price not selected
Input capacitors,3x4.7uF 25V,2x4.7uF 50V plus 100nF 50V target,,,TBD,verify effective capacitance and vendor bias curves
External compensation,R5 C9 C10 populated,DNP internal compensation,,,TBD,assembly and source BOM delta not quoted
Output capacitors,4x100uF 10V provisional,retain 4x100uF until measured,,,TBD,do not count any cap savings before bias/load-step evidence
PCB and assembly,baseline board/layout,new WSON layout and thermal vias,,,TBD,PCB CAM/DFM and factory quote not obtained
""", encoding="utf-8")
    (OUT / "power-budget-screen.csv").write_text(
        """metric,calculation,result,status
12V adapter input,12 V x 1.7 A,20.4 W,listing claim not measured
15V adapter input,15 V x 1.4 A,21.0 W,listing claim not measured
ideal 5V output current at 12V adapter,20.4 W / 5 V,4.08 A,ignores conversion loss and other loads
ideal 5V output current at 15V adapter,21 W / 5 V,4.20 A,ignores conversion loss and other loads
61 LED channels component maximum,61 x 38 mA,2.318 A,component maximum not product operating point
LED rail component-max screen,5 V x 2.318 A,11.590 W,not measured product load
adapter-based maximum continuous budget,not determined,NOT_FROZEN,requires actual input profile and camera/display/LED loads
""", encoding="utf-8")
    (OUT / "README.md").write_text(
        f"""# AI 美甲灯 LMR51450 电源候选 A1

状态：隔离原理图和封装候选；**PCB 未实现，不可制板/量产**。

此候选从本地 AI 灯电源 A1 原理图复制生成，不改写现有工程。变更将 TPS54540DDAR 加外置 PDS760 异步降压级，改为 LMR51450SDRRR 同步降压；输入目标为商品页所述 12 V 或 15 V，输出 5 V。VIN 的 10/11/12 脚、SW 的 1/2/3 脚及其余电源控制脚已逐脚导出网表并校验。EN 分压仍用 68.1 kΩ/10.0 kΩ，按 1.25 V 典型阈值计算上升门槛约 9.76 V；RT 开路，目标约 500 kHz。

器件初选沿用 TI LMR51450 5 V 设计章节：4.7 µH 电感，饱和电流至少 10 A、RMS 至少 6 A；100 kΩ/19.1 kΩ 反馈分压；BOOT 100 nF；同步整流后将 PDS760 设为 DNP。旧控制环路 R5/C9/C10 和 400 kHz RT 电阻 R1 标为 DNP。输入陶瓷电容候选调整为两颗 4.7 µF/50 V 加一颗 100 nF/50 V。原 4 颗 100 µF 输出陶瓷电容保持不变，尚未验证 5 V 偏压下的有效容量。

专用 footprint 的 13 个焊盘号、封装外形和中心散热焊盘按 TI DRR0012G 数据手册尺寸生成；中心焊盘的钢网开窗拆成三窗。PCB 尚未放置该封装，也未完成散热过孔、地平面、功率回路、反馈采样、间距、DRC 或 DFM。TI 的 LMR51450EVM 使用四层板和多层热扩散；因此不能仅凭这份原理图判断两层低价板是否能满足温升或 EMI。

## 本地静态验证

- KiCad 10.0.6 原理图网表导出：通过，U1 13 个焊盘的网络映射与候选功能相符；PG 脚明确不接。
- KiCad ERC：{result['erc_counts']['errors']} 个错误、{result['erc_counts']['warnings']} 条警告。
- 封装静态校验：焊盘 1–13 齐全；DRR0012G 外形 3.0 mm × 3.0 mm；中心裸焊盘 1.3 mm × 2.5 mm。
- 物理验证：没有原机或候选样品，未执行。

## 成本边界

按 A11 在 2026-10-10 留存的公开目录价快照，TPS54540DDAR 与 PDS760-13 的参考价合计相对 LMR51450SDRRR 约差 US$0.834/台，即 1,000 台约 US$834。PDS760 页面当时显示缺货。这只是指定器件行的目录价差，不包含新无源器件、PCB 层数/铜厚、装配、良率、测试及采购报价，不是整机降本承诺。完整成本仍未确定。

## 必须完成的下一轮

1. 基于完整灯具结构核对电源板外形、安装孔、连接器和摄像头/LED线束；完成新的 PCB 放置、回路布线和热过孔。
2. 重跑 ERC/DRC、网表-焊盘逐脚一致性、CAM/DFM；确认输入纹波、电感饱和和输出电容有效值。
3. 首轮样板限流上电；测试 12 V/15 V 启动、5 V 过冲/纹波/阶跃、相机显示负载、LED 分档负载、40 °C 热稳态、短路/掉电恢复、EMI。
4. 用准确原机与候选整灯做同一适配器、几何、UV 辐照地图、固化、温度和用户模式 A/B。
5. 以正式 1,000 台报价比较器件、板厂、装配和测试总成本，再决定是否替换原 buck。

## 来源

- [TI LMR51450 数据手册](https://www.ti.com/lit/ds/symlink/lmr51450.pdf)
- [TI LMR51450EVM 用户指南](https://www.ti.com/lit/pdf/SNVU838)
- Amazon AI 灯商品页：<https://www.amazon.com/dp/B0GR425DGM>
""", encoding="utf-8")
    manifest_files = [
        "lamp_buck_lmr51450_a1.kicad_sch", "lamp_buck_lmr51450_a1.kicad_pro",
        "Lamp_Power.pretty/LMR51450_WSON_DRR0012.kicad_mod", "lamp_buck_lmr51450_a1.net.xml",
        "lamp_buck_lmr51450_a1.erc.rpt", "verification.json", "BOM-delta-1000pcs.csv",
        "power-budget-screen.csv", "README.md", "fp-lib-table", "sym-lib-table", "Project.kicad_sym",
    ]
    manifest_files.extend(
        str(path.relative_to(OUT)) for path in sorted((OUT / "Lamp_Power.pretty").glob("*.kicad_mod"))
        if str(path.relative_to(OUT)) not in manifest_files
    )
    (OUT / "MANIFEST-SHA256.txt").write_text(
        "".join(f"{hashlib.sha256((OUT / name).read_bytes()).hexdigest()}  {name}\n" for name in manifest_files),
        encoding="utf-8",
    )
def export_and_verify() -> dict:
    netlist = OUT / "lamp_buck_lmr51450_a1.net.xml"
    erc = OUT / "lamp_buck_lmr51450_a1.erc.rpt"
    subprocess.run([str(KICAD_CLI), "sch", "export", "netlist", "--format", "kicadxml",
                    "--output", str(netlist), str(SCH)], check=True, capture_output=True, text=True)
    subprocess.run([str(KICAD_CLI), "sch", "erc", "--output", str(erc), str(SCH)],
                   check=True, capture_output=True, text=True)
    root = ET.parse(netlist).getroot()
    nets: dict[str, set[tuple[str, str]]] = {}
    for net in root.findall("./nets/net"):
        nets[net.attrib["name"]] = {
            (node.attrib["ref"], node.attrib["pin"]) for node in net.findall("node")
        }
    u1_pins: dict[str, list[str]] = {}
    for name, members in nets.items():
        for ref, pin in members:
            if ref == "U1":
                u1_pins.setdefault(pin, []).append(name)
    expected = {
        "VIN": {"10", "11", "12"}, "SW": {"1", "2", "3"},
        "EN": {"9"}, "RT": {"6"}, "FB": {"7"},
        "BOOT": {"4"}, "PGND": {"13"}, "AGND": {"8"},
    }
    pin_name = {"VIN": "/VIN_PD_12_15", "SW": "/SW", "EN": "/BUCK_EN_UVLO",
                "RT": "/RT_OPEN_500KHZ", "FB": "/FB", "BOOT": "/BOOT",
                "PGND": "/GND", "AGND": "/GND"}
    for group, pins in expected.items():
        for pin in pins:
            assert pin in u1_pins, f"missing U1 pin {pin} in netlist"
            assert pin_name[group] in u1_pins[pin], f"U1 pin {pin} expected {pin_name[group]}, got {u1_pins[pin]}"
    assert any(name.startswith("unconnected-(U1-PG-") for name in u1_pins.get("5", [])), \
        "PG pin should be explicitly no-connect"
    erc_text = erc.read_text(encoding="utf-8")
    erc_match = re.search(r"ERC messages:\s*(\d+)\s+Errors\s+(\d+)\s+Warnings\s+(\d+)", erc_text)
    assert erc_match and erc_match.group(2) == "0", "KiCad ERC has errors"
    fp_text = FP.read_text(encoding="utf-8")
    fp_pads = re.findall(r'\(pad "(\d+)" smd', fp_text)
    assert set(fp_pads) == {str(n) for n in range(1, 14)}
    assert len(fp_pads) == 13 and fp_text.count('(layer "F.Paste")') == 3
    return {
        "netlist": str(netlist),
        "erc": str(erc),
        "u1_pin_nets": {pin: sorted(names) for pin, names in sorted(u1_pins.items())},
        "erc_text": erc_text,
        "erc_counts": {"errors": int(erc_match.group(2)), "warnings": int(erc_match.group(3))},
        "footprint": str(FP),
        "footprint_pad_numbers": sorted(map(int, fp_pads)),
        "footprint_body_mm": [3.0, 3.0],
        "footprint_exposed_pad_mm": [1.3, 2.5],
        "netlist_sha256": hashlib.sha256(netlist.read_bytes()).hexdigest(),
    }


def main() -> None:
    if OUT.exists():
        shutil.rmtree(OUT)
    OUT.mkdir(parents=True)
    shutil.copy2(SOURCE.parent / "lamp_power_evt_a1_compact_bench.kicad_pro",
                 OUT / "lamp_buck_lmr51450_a1.kicad_pro")
    make_footprint()
    patch_schematic()
    create_external_libraries()
    result = export_and_verify()
    source_hash = hashlib.sha256(SOURCE.read_bytes()).hexdigest()
    result.update({
        "status": "SCHEMATIC_NETLIST_PASS_ERC_REVIEW_REQUIRED_PCB_NOT_IMPLEMENTED",
        "source_schematic_sha256": source_hash,
        "candidate_schematic_sha256": hashlib.sha256(SCH.read_bytes()).hexdigest(),
        "candidate_footprint": "Lamp_Power:LMR51450_WSON_DRR0012",
        "pcb_status": "NOT_CREATED",
        "physical_validation": "NOT_RUN_NO_SAMPLE",
    })
    (OUT / "verification.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n",
                                              encoding="utf-8")
    write_candidate_docs(result)
    print(json.dumps({key: result[key] for key in ("status", "u1_pin_nets", "candidate_schematic_sha256", "netlist_sha256")},
                     ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
