"""Single-point-of-failure on power rails — REVIEW, never RULE ERROR. FACT: how many regulators drive a rail and how many ICs sit on it. REQUIREMENT: redundancy is not a datasheet shall unless stated. INFERENCE: this rail is a SPOF — review, do not invent IEC SIL. """ from __future__ import annotations from collections import defaultdict from backend.periscopex.models import ( ComponentConstraints, ComponentType, DesignGraph, Finding, NetType, ) from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net from backend.periscopex.power_margin_check import _regulators _SKIP_RAIL = frozenset({"GND", "AGND", "DGND", "PGND", "VSS"}) def check_spof( graph: DesignGraph, constraints_map: dict[str, ComponentConstraints] | None = None, ) -> list[Finding]: cmap = constraints_map or {} producers: dict[str, list[str]] = defaultdict(list) for ref, _comp, _cons, _vin, vout in _regulators(graph, cmap): key = normalize_kicad_hierarchy_net(vout) if key.upper() in _SKIP_RAIL: continue producers[key].append(ref) out: list[Finding] = [] seen: set[str] = set() for net_name, net in sorted(graph.nets.items()): key = normalize_kicad_hierarchy_net(net_name) if key in seen or key.upper() in _SKIP_RAIL: continue if net.net_type != NetType.POWER and not producers.get(key): continue regs = producers.get(key) or [] if len(regs) != 1: continue ics = [ r for r in graph.components_on_net(net_name) if r in graph.components and graph.components[r].component_type == ComponentType.IC and r not in regs ] if len(ics) < 1: continue seen.add(key) reg = regs[0] rcomp = graph.components[reg] rec = ( f"Review whether '{net_name}' needs a second source; {reg} is the " f"only regulator feeding {len(ics)} IC(s). This is not a design-rule ERROR." ) out.append(Finding( designator=reg, mpn=rcomp.mpn or "", aspect="spof", finding=( f"Rail '{net_name}' has a single regulator ({reg}) and " f"{len(ics)} downstream IC(s) ({', '.join(ics[:8])}" f"{'…' if len(ics) > 8 else ''})." ), facts=( f"producers={regs}; downstream_ics={ics}; " f"net_type={net.net_type.value}." ), requirement=( "Redundancy / dual-supply is REVIEW unless a datasheet shall " "names a second source (none assumed)." ), inference="Single point of failure on this rail — engineering review.", why="SPOF is not a measured RULE; no IEC SIL invented.", status="INFO", recommendation=rec, action=rec, source="spof_check", rule_id="PE-SPOF-001", evidence_status="SUFFICIENT", net=net_name, pins=[], )) # Crystal / clock: one XTAL feeding one MCU is normal — only flag when # several ICs share one crystal net with no second oscillator FACT. for ref, comp in sorted(graph.components.items()): if comp.component_type != ComponentType.CRYSTAL: continue nets = [n for n in set(comp.pins.values()) if n] ics: list[str] = [] for n in nets: for r in graph.components_on_net(n): c = graph.components.get(r) if c and c.component_type == ComponentType.IC and r not in ics: ics.append(r) xtals = [ r for r, c in graph.components.items() if c.component_type == ComponentType.CRYSTAL ] if len(ics) < 2 or len(xtals) > 1: continue rec = ( f"Review clock redundancy: {ref} is the only crystal and feeds " f"{len(ics)} ICs. Not a RULE." ) out.append(Finding( designator=ref, mpn=comp.mpn or "", aspect="spof", finding=( f"{ref} is the only crystal and clocks {len(ics)} ICs " f"({', '.join(ics)})." ), facts=f"xtals={xtals}; ics_on_crystal_nets={ics}.", requirement="A second oscillator is not assumed from IEC or folklore.", inference="Clock SPOF — REVIEW.", why="Counted crystals and IC pins on those nets.", status="INFO", recommendation=rec, action=rec, source="spof_check", rule_id="PE-SPOF-001", evidence_status="SUFFICIENT", pins=[], )) return out