main
TylerK Board update: 4 unconnected items (from 13), 0 electrical and 0 mechanical DRC findings, silkscreen findings 73 -> 10;… 337e32d 1mo ago
#!/usr/bin/env python3
"""One-command board verdict: runs KiCad DRC with zone refill, then classifies
every finding as ELECTRICAL (must fix), MECHANICAL (should fix) or COSMETIC.

Exists because sampling a DRC report by eye is how 86 genuine different-net
shorts got mischaracterised as harmless same-net warnings earlier in this
project. Every finding is now classified programmatically, and the
different-net check is explicit rather than assumed.
"""
import re, subprocess, sys
from collections import Counter

KICAD = "/mnt/c/Program Files/KiCad/10.0/bin/kicad-cli.exe"
PCB_WIN = r"C:\Users\kirkt\Documents\mppt-power-system-mcu\mppt-power-system-mcu.kicad_pcb"
RPT_WIN = r"C:\Users\kirkt\Documents\mppt-power-system-mcu\drc_check.rpt"
RPT = "/mnt/c/Users/kirkt/Documents/mppt-power-system-mcu/drc_check.rpt"

ELECTRICAL = {"shorting_items", "clearance", "tracks_crossing", "unconnected_items",
              "hole_clearance", "solder_mask_bridge", "starved_thermal"}
MECHANICAL = {"hole_to_hole", "holes_co_located", "annular_width", "track_dangling",
              "isolated_copper", "courtyards_overlap", "malformed_courtyard"}
COSMETIC = {"silk_overlap", "silk_over_copper", "lib_footprint_mismatch",
            "footprint_symbol_mismatch", "extra_footprint", "silk_edge_clearance"}


def nets_in(entry):
    """Net names referenced by the two offending items."""
    out = []
    for line in entry.split("\n")[1:]:
        m = re.search(r'\[([^\]]+)\]', line)
        if m: out.append(m.group(1))
    return out


def main(refill=True):
    cmd = [KICAD, "pcb", "drc"]
    if refill: cmd += ["--refill-zones", "--save-board"]
    cmd += [PCB_WIN, "-o", RPT_WIN]
    r = subprocess.run(cmd, capture_output=True, text=True)
    head = [l for l in (r.stdout or "").splitlines() if "Found" in l]
    txt = open(RPT, errors="replace").read()
    entries = re.split(r'\n(?=\[\w+\]:)', txt)

    kinds = Counter()
    diff_net = Counter()
    same_net = Counter()
    worst = []
    for e in entries:
        m = re.match(r'\[(\w+)\]', e)
        if not m: continue
        k = m.group(1); kinds[k] += 1
        ns = set(nets_in(e))
        if k in ELECTRICAL and k != "unconnected_items":
            if len(ns) > 1:
                diff_net[k] += 1
                a = re.search(r'actual ([\d.]+) mm', e)
                worst.append((float(a.group(1)) if a else 0.0, k, sorted(ns)))
            else:
                same_net[k] += 1

    e_tot = sum(v for k, v in kinds.items() if k in ELECTRICAL)
    m_tot = sum(v for k, v in kinds.items() if k in MECHANICAL)
    c_tot = sum(v for k, v in kinds.items() if k in COSMETIC)
    other = sum(v for k, v in kinds.items()
                if k not in ELECTRICAL | MECHANICAL | COSMETIC)

    for l in head: print(l)
    print(f"\n  ELECTRICAL (must fix) : {e_tot}")
    for k in sorted(ELECTRICAL):
        if kinds.get(k):
            d = diff_net.get(k, 0); s = same_net.get(k, 0)
            extra = f"   [different-net: {d}, same-net: {s}]" if k != "unconnected_items" else ""
            print(f"      {k:22s} {kinds[k]:4d}{extra}")
    print(f"  MECHANICAL (should fix): {m_tot}")
    for k in sorted(MECHANICAL):
        if kinds.get(k): print(f"      {k:22s} {kinds[k]:4d}")
    print(f"  COSMETIC               : {c_tot}")
    for k in sorted(COSMETIC):
        if kinds.get(k): print(f"      {k:22s} {kinds[k]:4d}")
    if other: print(f"  UNCLASSIFIED           : {other}")

    real = diff_net_total = sum(diff_net.values())
    unconn = kinds.get("unconnected_items", 0)
    print(f"\n  >>> genuine different-net electrical defects: {real}")
    print(f"  >>> unconnected items: {unconn}")
    if worst:
        worst.sort()
        print("\n  tightest different-net clearances:")
        for a, k, ns in worst[:8]:
            print(f"      {a:.3f}mm  {k:18s} {ns}")
    return real + unconn


if __name__ == "__main__":
    sys.exit(0 if main() == 0 else 1)