"""Interface class certifiers — USB-C, Ethernet 10/100 vs GbE, PoE. First slice: class + connection integrity. Not generic SI, not via ampacity, not thermal FEM. Missing evidence is INSUFFICIENT or N/A, never invented geometry / Z / I / PoE / SuperSpeed. """ from __future__ import annotations from backend.periscopex.finding_engine import complete_finding, lookup_rule from backend.periscopex.interface_class_check import check_interface_classes from backend.periscopex.models import ( Component, ComponentType, DesignGraph, Net, NetType, PinConnection, ResistorSpecs, ) from backend.periscopex.pcb_checks import merge_schema_pcb_reports, run_pcb_checks from tests.paths import SIMPLE_PROJECT def _comp( ref: str, *, ctype: ComponentType, value: str = "", footprint: str = "", mpn: str = "", pins: dict[str, str] | None = None, subtype: str | None = None, specs=None, ) -> Component: return Component( reference=ref, value=value, footprint=footprint, component_type=ctype, component_subtype=subtype, mpn=mpn or None, pins=pins or {}, specs=specs, ) def _net(name: str, ntype: NetType, *refs_pins: tuple[str, str]) -> Net: return Net( name=name, net_type=ntype, pins=[PinConnection(component_ref=r, pin_number=p) for r, p in refs_pins], ) def _by_rule(findings, rule_id: str, designator: str | None = None): out = [ f for f in findings if f.rule_id == rule_id and (designator is None or f.designator == designator) ] return out def _rd(ref: str, cc_net: str) -> Component: return _comp( ref, ctype=ComponentType.RESISTOR, value="5k1", pins={"1": cc_net, "2": "GND"}, specs=ResistorSpecs( value_ohms=5100.0, value_formatted="5.1k", ), ) def _usbc_device_graph(*, ss: bool = False, usb2_value: bool = True) -> DesignGraph: """USB-C receptacle with Rd 5.1 kΩ on CC1/CC2, VBUS, GND.""" value = "USB_C_Receptacle_USB2.0_16P" if usb2_value else "USB_C_Receptacle_USB3.1" pins = { "A5": "USB_CC1", "B5": "USB_CC2", "A4": "VBUS", "A9": "VBUS", "A1": "GND", "A12": "GND", "A6": "USB_D+", "A7": "USB_D-", } nets = { "USB_CC1": _net("USB_CC1", NetType.SIGNAL, ("J2", "A5"), ("R10", "1")), "USB_CC2": _net("USB_CC2", NetType.SIGNAL, ("J2", "B5"), ("R11", "1")), "VBUS": _net("VBUS", NetType.POWER, ("J2", "A4"), ("J2", "A9")), "GND": _net("GND", NetType.GROUND, ("J2", "A1"), ("J2", "A12"), ("R10", "2"), ("R11", "2")), "USB_D+": _net("USB_D+", NetType.SIGNAL, ("J2", "A6")), "USB_D-": _net("USB_D-", NetType.SIGNAL, ("J2", "A7")), } comps = { "J2": _comp( "J2", ctype=ComponentType.CONNECTOR, value=value, footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12", mpn="TYPE-C-31-M-12", subtype="connector.usb", pins=pins, ), "R10": _rd("R10", "USB_CC1"), "R11": _rd("R11", "USB_CC2"), } if ss: pins["A2"] = "USB_SSTX_P" pins["A3"] = "USB_SSTX_N" pins["B11"] = "USB_SSRX_P" pins["B10"] = "USB_SSRX_N" nets["USB_SSTX_P"] = _net("USB_SSTX_P", NetType.SIGNAL, ("J2", "A2")) nets["USB_SSTX_N"] = _net("USB_SSTX_N", NetType.SIGNAL, ("J2", "A3")) nets["USB_SSRX_P"] = _net("USB_SSRX_P", NetType.SIGNAL, ("J2", "B11")) nets["USB_SSRX_N"] = _net("USB_SSRX_N", NetType.SIGNAL, ("J2", "B10")) comps["J2"] = _comp( "J2", ctype=ComponentType.CONNECTOR, value="USB_C_Receptacle_USB3.1", footprint="Connector_USB:USB_C_Receptacle_24P", mpn="USB3-C-24", subtype="connector.usb", pins=pins, ) return DesignGraph(components=comps, nets=nets) def test_no_connector_is_na_not_error(): g = DesignGraph( components={ "U1": _comp("U1", ctype=ComponentType.IC, value="MCU", pins={"1": "GND"}), }, nets={"GND": _net("GND", NetType.GROUND, ("U1", "1"))}, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) assert _by_rule(findings, "PE-USBC-001") assert _by_rule(findings, "PE-ETH-001") assert _by_rule(findings, "PE-POE-001") assert all(f.status != "ERROR" for f in findings) assert all(f.finding_class == "INFO" for f in findings) assert "N/A" in _by_rule(findings, "PE-USBC-001")[0].finding assert "N/A" in _by_rule(findings, "PE-ETH-001")[0].finding assert "N/A" in _by_rule(findings, "PE-POE-001")[0].finding def test_usbc_rd_vbus_gnd_certified(): findings = check_interface_classes(_usbc_device_graph()) for f in findings: complete_finding(f) cls = _by_rule(findings, "PE-USBC-001", "J2")[0] assert cls.status == "INFO" assert "USB2" in cls.finding assert _by_rule(findings, "PE-USBC-002", "J2")[0].status == "INFO" rd = _by_rule(findings, "PE-USBC-003", "J2")[0] assert rd.status == "INFO" assert rd.evidence_status == "SUFFICIENT" assert "5100" in rd.facts or "5.1" in rd.facts assert _by_rule(findings, "PE-USBC-004", "J2")[0].status == "INFO" ss = _by_rule(findings, "PE-USBC-005", "J2")[0] assert ss.status != "ERROR" assert ss.finding_class == "INFO" assert "N/A" in ss.finding or "USB2" in ss.finding assert all(f.status != "ERROR" for f in findings if f.designator == "J2") def test_usbc_missing_cc2_is_error(): g = _usbc_device_graph() j2 = g.components["J2"] pins = dict(j2.pins) pins.pop("B5") g.components["J2"] = j2.model_copy(update={"pins": pins}) del g.nets["USB_CC2"] findings = check_interface_classes(g) for f in findings: complete_finding(f) cc = _by_rule(findings, "PE-USBC-002", "J2")[0] assert cc.status == "ERROR" assert cc.finding_class == "RULE" assert cc.evidence_status == "SUFFICIENT" def test_usbc_wrong_rd_is_error(): g = _usbc_device_graph() g.components["R10"] = _comp( "R10", ctype=ComponentType.RESISTOR, value="10k", pins={"1": "USB_CC1", "2": "GND"}, specs=ResistorSpecs(value_ohms=10000.0, value_formatted="10k"), ) findings = check_interface_classes(g) for f in findings: complete_finding(f) rd = _by_rule(findings, "PE-USBC-003", "J2")[0] assert rd.status == "ERROR" assert rd.finding_class == "RULE" def test_usbc_cc_to_controller_without_r_is_insufficient_not_error(): g = _usbc_device_graph() del g.components["R10"] del g.components["R11"] g.components["U5"] = _comp( "U5", ctype=ComponentType.IC, value="CC controller", mpn="CC-IC", pins={"1": "USB_CC1", "2": "USB_CC2"}, ) g.nets["USB_CC1"] = _net("USB_CC1", NetType.SIGNAL, ("J2", "A5"), ("U5", "1")) g.nets["USB_CC2"] = _net("USB_CC2", NetType.SIGNAL, ("J2", "B5"), ("U5", "2")) findings = check_interface_classes(g) for f in findings: complete_finding(f) rd = _by_rule(findings, "PE-USBC-003", "J2")[0] assert rd.status != "ERROR" assert rd.evidence_status == "INSUFFICIENT" assert rd.finding_class == "INFO" def test_usbc_unknown_r_value_is_insufficient(): g = _usbc_device_graph() g.components["R10"] = _comp( "R10", ctype=ComponentType.RESISTOR, value="", pins={"1": "USB_CC1", "2": "GND"}, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) rd = _by_rule(findings, "PE-USBC-003", "J2")[0] assert rd.status != "ERROR" assert rd.evidence_status == "INSUFFICIENT" def test_usb3_class_without_ss_pairs_is_error(): g = _usbc_device_graph(usb2_value=False) j2 = g.components["J2"] g.components["J2"] = j2.model_copy(update={ "value": "USB_C_Receptacle_USB3.1", "footprint": "Connector_USB:USB_C_Receptacle_24P", "mpn": "USB3-C", }) findings = check_interface_classes(g) for f in findings: complete_finding(f) ss = _by_rule(findings, "PE-USBC-005", "J2")[0] assert ss.status == "ERROR" assert ss.finding_class == "RULE" assert "SuperSpeed" in ss.finding or "USB3" in ss.finding def test_usb3_with_ss_pairs_is_certified(): findings = check_interface_classes(_usbc_device_graph(ss=True, usb2_value=False)) for f in findings: complete_finding(f) ss = _by_rule(findings, "PE-USBC-005", "J2")[0] assert ss.status == "INFO" assert ss.evidence_status == "SUFFICIENT" assert ss.status != "ERROR" def test_missing_vbus_is_error(): g = _usbc_device_graph() j2 = g.components["J2"] pins = {k: v for k, v in j2.pins.items() if v != "VBUS"} g.components["J2"] = j2.model_copy(update={"pins": pins}) findings = check_interface_classes(g) for f in findings: complete_finding(f) pwr = _by_rule(findings, "PE-USBC-004", "J2")[0] assert pwr.status == "ERROR" assert pwr.finding_class == "RULE" def _rj45( *, value: str, mpn: str, pins: dict[str, str], extra_comps: dict[str, Component] | None = None, extra_nets: dict[str, Net] | None = None, ) -> DesignGraph: comps = { "J1": _comp( "J1", ctype=ComponentType.CONNECTOR, value=value, footprint="Connector_RJ:RJ45", mpn=mpn, pins=pins, ), } nets = {} for pin, net in pins.items(): nets.setdefault(net, Net(name=net, net_type=NetType.SIGNAL, pins=[])) nets[net].pins.append(PinConnection(component_ref="J1", pin_number=pin)) if net.upper() in {"GND", "AGND"}: nets[net].net_type = NetType.GROUND if extra_comps: comps.update(extra_comps) if extra_nets: for n, net in extra_nets.items(): if n in nets: nets[n].pins.extend(net.pins) else: nets[n] = net return DesignGraph(components=comps, nets=nets) def test_ethernet_10_100_magjack_no_gbe_magnetics_error(): g = _rj45( value="RJ45 PoE 10/100 Base-TX Jack with Magnetic Module", mpn="ARJP11A-MASA-B-A-EMU2", pins={ "1": "ETH_TX+", "2": "ETH_TX-", "3": "ETH_RX+", "6": "ETH_RX-", }, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) cls = _by_rule(findings, "PE-ETH-001", "J1")[0] assert cls.status == "INFO" assert "10/100" in cls.finding assert "GbE" not in cls.finding or "not GbE" in cls.finding.lower() or "10/100" in cls.finding pairs = _by_rule(findings, "PE-ETH-002", "J1")[0] assert pairs.status == "INFO" mag = _by_rule(findings, "PE-ETH-003", "J1")[0] assert mag.status != "ERROR" assert mag.finding_class == "INFO" assert "N/A" in mag.finding or "10/100" in mag.finding def test_gbe_without_magnetics_is_error(): g = _rj45( value="RJ45 1000BASE-T", mpn="RJ45-GBE-BARE", pins={ "1": "TRD0_P", "2": "TRD0_N", "3": "TRD1_P", "6": "TRD1_N", "4": "TRD2_P", "5": "TRD2_N", "7": "TRD3_P", "8": "TRD3_N", }, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) cls = _by_rule(findings, "PE-ETH-001", "J1")[0] assert "GbE" in cls.finding or "1000" in cls.finding mag = _by_rule(findings, "PE-ETH-003", "J1")[0] assert mag.status == "ERROR" assert mag.finding_class == "RULE" assert mag.evidence_status == "SUFFICIENT" def test_gbe_with_magnetics_is_certified(): g = _rj45( value="RJ45 1000BASE-T MagJack", mpn="PULSE-GBE-MAG", pins={ "1": "TRD0_P", "2": "TRD0_N", "3": "TRD1_P", "6": "TRD1_N", "4": "TRD2_P", "5": "TRD2_N", "7": "TRD3_P", "8": "TRD3_N", }, extra_comps={ "T1": _comp( "T1", ctype=ComponentType.TRANSFORMER, value="LAN magnetics", mpn="HX1198FNL", subtype="transformer.signal", pins={"1": "TRD0_P", "2": "TRD0_N"}, ), }, extra_nets={ "TRD0_P": _net("TRD0_P", NetType.SIGNAL, ("T1", "1")), }, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) mag = _by_rule(findings, "PE-ETH-003", "J1")[0] assert mag.status == "INFO" assert mag.evidence_status == "SUFFICIENT" assert mag.status != "ERROR" def test_rj45_without_speed_is_insufficient_not_invented_gbe(): g = _rj45( value="RJ45", mpn="8P8C", pins={"1": "NET1", "2": "NET2", "3": "NET3", "6": "NET6"}, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) cls = _by_rule(findings, "PE-ETH-001", "J1")[0] assert cls.status != "ERROR" assert cls.evidence_status == "INSUFFICIENT" mag = _by_rule(findings, "PE-ETH-003", "J1")[0] assert mag.status != "ERROR" assert mag.finding_class == "INFO" def test_bare_rj45_does_not_invent_poe(): g = _rj45( value="RJ45", mpn="8P8C-BARE", pins={"1": "ETH_TX+", "2": "ETH_TX-", "3": "ETH_RX+", "6": "ETH_RX-"}, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) poe = _by_rule(findings, "PE-POE-001", "J1")[0] assert poe.status != "ERROR" assert poe.finding_class == "INFO" assert "N/A" in poe.finding assert not _by_rule(findings, "PE-POE-003", "J1") or _by_rule( findings, "PE-POE-003", "J1", )[0].status != "ERROR" def test_poe_with_evidence_requires_magnetics_isolation(): g = _rj45( value="RJ45 PoE 10/100 Base-TX Jack with Magnetic Module", mpn="ARJP11A-MASA-B-A-EMU2", pins={ "1": "ETH_TX+", "2": "ETH_TX-", "3": "ETH_RX+", "6": "ETH_RX-", }, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) ev = _by_rule(findings, "PE-POE-001", "J1")[0] assert ev.status == "INFO" assert ev.evidence_status == "SUFFICIENT" assert "PoE" in ev.finding cls = _by_rule(findings, "PE-POE-002", "J1")[0] assert cls.status != "ERROR" assert cls.evidence_status == "INSUFFICIENT" iso = _by_rule(findings, "PE-POE-003", "J1")[0] assert iso.status == "INFO" assert iso.evidence_status == "SUFFICIENT" def test_poe_evidence_on_bare_jack_is_isolation_error(): g = _rj45( value="RJ45 PoE 802.3af", mpn="RJ45-POE-BARE", pins={"1": "ETH_TX+", "2": "ETH_TX-", "3": "ETH_RX+", "6": "ETH_RX-"}, ) findings = check_interface_classes(g) for f in findings: complete_finding(f) iso = _by_rule(findings, "PE-POE-003", "J1")[0] assert iso.status == "ERROR" assert iso.finding_class == "RULE" def test_simple_project_usbc_not_false_error(): graph = DesignGraph.model_validate_json( (SIMPLE_PROJECT / "design_graph.json").read_text() ) findings = check_interface_classes(graph) for f in findings: complete_finding(f) usbc = [f for f in findings if (f.rule_id or "").startswith("PE-USBC") and f.designator == "J1"] assert usbc assert all(f.status != "ERROR" for f in usbc) rd = _by_rule(findings, "PE-USBC-003", "J1")[0] assert rd.status == "INFO" assert rd.evidence_status == "SUFFICIENT" ss = _by_rule(findings, "PE-USBC-005", "J1")[0] assert ss.status != "ERROR" assert ss.evidence_status == "INSUFFICIENT" or "N/A" in ss.finding assert _by_rule(findings, "PE-ETH-001")[0].status != "ERROR" assert "N/A" in _by_rule(findings, "PE-ETH-001")[0].finding assert _by_rule(findings, "PE-POE-001")[0].status != "ERROR" assert "N/A" in _by_rule(findings, "PE-POE-001")[0].finding def test_hubaudio_like_usbc_usb2_and_poe_magjack(): """HubAudio J2 is USB2 Type-C 16P; J1 is PoE 10/100 MagJack — no false SS/GbE/PoE class.""" usbc = _usbc_device_graph() eth = _rj45( value="RJ45 PoE 10/100 Base-TX Jack with Magnetic Module", mpn="ARJP11A-MASA-B-A-EMU2", pins={ "1": "ETH_TX+", "2": "ETH_TX-", "3": "ETH_RX+", "6": "ETH_RX-", }, ) comps = dict(usbc.components) comps.update(eth.components) nets = dict(usbc.nets) for name, net in eth.nets.items(): if name in nets: nets[name] = nets[name].model_copy( update={"pins": list(nets[name].pins) + list(net.pins)}, ) else: nets[name] = net g = DesignGraph(components=comps, nets=nets) findings = check_interface_classes(g) for f in findings: complete_finding(f) assert _by_rule(findings, "PE-USBC-005", "J2")[0].status != "ERROR" assert _by_rule(findings, "PE-ETH-001", "J1")[0].status != "ERROR" assert _by_rule(findings, "PE-ETH-003", "J1")[0].status != "ERROR" assert _by_rule(findings, "PE-POE-001", "J1")[0].status != "ERROR" assert _by_rule(findings, "PE-POE-002", "J1")[0].status != "ERROR" assert _by_rule(findings, "PE-POE-003", "J1")[0].status != "ERROR" assert all( f.status != "ERROR" for f in findings if (f.rule_id or "").startswith(("PE-USBC", "PE-ETH", "PE-POE")) ) def test_rule_catalog_shared_not_si(): for rid in ( "PE-USBC-001", "PE-USBC-002", "PE-USBC-003", "PE-USBC-004", "PE-USBC-005", "PE-ETH-001", "PE-ETH-002", "PE-ETH-003", "PE-ETH-004", "PE-POE-001", "PE-POE-002", "PE-POE-003", ): rec = lookup_rule(rid) assert rec is not None, rid assert rec.domain == "shared" def test_run_pcb_checks_includes_interface_class(): findings = run_pcb_checks(DesignGraph(), {}, None) ids = {f.rule_id for f in findings} assert "PE-USBC-001" in ids assert "PE-ETH-001" in ids assert "PE-POE-001" in ids if_f = [f for f in findings if (f.rule_id or "").startswith(("PE-USBC", "PE-ETH", "PE-POE"))] assert if_f assert all(f.status != "ERROR" for f in if_f) assert all((f.finding_id or "").startswith("PCB-") for f in if_f) def test_merge_drops_duplicate_pcb_interface_findings(): schema = { "findings": [{ "finding_id": "J2-001", "designator": "J2", "finding": "class USB-C USB2", "status": "INFO", "rule_id": "PE-USBC-001", "source": "interface_class_check", }], "summary": {"INFO": 1, "ERROR": 0, "WARNING": 0}, } pcb = { "findings": [{ "finding_id": "PCB-J2-001", "designator": "J2", "finding": "class USB-C USB2", "status": "INFO", "rule_id": "PE-USBC-001", "source": "interface_class_check", }], "summary": {"INFO": 1, "ERROR": 0, "WARNING": 0}, } merged = merge_schema_pcb_reports(schema, pcb) usbc = [ f for f in merged["findings"] if f.get("rule_id") == "PE-USBC-001" and f.get("designator") == "J2" ] assert len(usbc) == 1 assert usbc[0]["finding_id"] == "J2-001" def test_does_not_use_layout_geometry(): """Certifier takes the netlist graph only — no invented mm / Z / I.""" import inspect from backend.periscopex import interface_class_check as mod sig = inspect.signature(mod.check_interface_classes) assert list(sig.parameters) == ["graph"] src = inspect.getsource(mod) assert "LayoutVia" not in src assert "LayoutPad" not in src assert "LayoutSegment" not in src assert "LayoutZone" not in src