Gate interface certifiers to connectors on this board (2.61.2).

USB-C/RJ45/PoE/DDR3 are the method, not a catalog. No connector means
no finding. Bare RJ45 is Ethernet only; PoE needs PoE evidence. HubAudio
has USB-C and PoE MagJack, not DDR. Skip-I unchanged.
This commit is contained in:
2026-09-21 12:24:54 +02:00
parent c6abdae762
commit bd3a6fd009
14 changed files with 84 additions and 97 deletions
+3 -3
View File
@@ -2,7 +2,7 @@
Favor: perfect golden match; citation rate ignores deterministic findings;
simple_project graph still has U1/U2/U3, the two I2C pull-up keys,
and USB-C class certifiers on J1 (Ethernet/PoE N/A).
and USB-C class certifiers on J1 (no Ethernet/PoE — simple_project has no RJ45).
Against: extra finding drops precision; missing golden key drops recall;
Unverified quotes are citation misses, not hits.
"""
@@ -88,9 +88,9 @@ def test_simple_project_eval_matches_committed_golden():
assert scores.graph_ok, scores.graph_errors
assert scores.precision == 1.0
assert scores.recall == 1.0
assert scores.finding_count == 10
assert scores.finding_count == 8
assert scores.by_status["WARNING"] == 2
assert scores.by_status["INFO"] == 8
assert scores.by_status["INFO"] == 6
def test_simple_project_eval_rejects_truncated_graph(tmp_path: Path):
+30 -30
View File
@@ -128,7 +128,8 @@ def _usbc_device_graph(*, ss: bool = False, usb2_value: bool = True) -> DesignGr
return DesignGraph(components=comps, nets=nets)
def test_no_connector_is_na_not_error():
def test_no_connector_emits_nothing():
"""No USB-C / no RJ45 / no PoE evidence → do not emit that certifier."""
g = DesignGraph(
components={
"U1": _comp("U1", ctype=ComponentType.IC, value="MCU", pins={"1": "GND"}),
@@ -136,16 +137,8 @@ def test_no_connector_is_na_not_error():
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
assert findings == []
assert not any((f.rule_id or "").startswith("PE-DDR") for f in findings)
def test_usbc_rd_vbus_gnd_certified():
@@ -166,6 +159,7 @@ def test_usbc_rd_vbus_gnd_certified():
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")
assert not any((f.rule_id or "").startswith(("PE-ETH", "PE-POE", "PE-DDR")) for f in findings)
def test_usbc_missing_cc2_is_error():
@@ -429,13 +423,11 @@ def test_bare_rj45_does_not_invent_poe():
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"
assert _by_rule(findings, "PE-ETH-001", "J1")
assert not any((f.rule_id or "").startswith("PE-POE") for f in findings)
assert not any((f.rule_id or "").startswith("PE-USBC") for f in findings)
assert not any((f.rule_id or "").startswith("PE-DDR") for f in findings)
assert all(f.status != "ERROR" for f in findings if (f.rule_id or "").startswith("PE-ETH"))
def test_poe_with_evidence_requires_magnetics_isolation():
@@ -494,10 +486,9 @@ def test_simple_project_usbc_not_false_error():
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
assert not any((f.rule_id or "").startswith("PE-ETH") for f in findings)
assert not any((f.rule_id or "").startswith("PE-POE") for f in findings)
assert not any((f.rule_id or "").startswith("PE-DDR") for f in findings)
def test_hubaudio_like_usbc_usb2_and_poe_magjack():
@@ -538,6 +529,17 @@ def test_hubaudio_like_usbc_usb2_and_poe_magjack():
for f in findings
if (f.rule_id or "").startswith(("PE-USBC", "PE-ETH", "PE-POE"))
)
assert not any((f.rule_id or "").startswith("PE-DDR") for f in findings)
def test_hubaudio_bom_has_usbc_and_poe_rj45_not_ddr():
"""HubAudio actual parts: J2 USB-C USB2 16P, J1 PoE 10/100 MagJack, no DDR."""
from tests.paths import REPO_ROOT
bom = (REPO_ROOT / "tests" / "fixtures" / "hubaudio_kicad_bom.csv").read_text()
assert "USB_C_Receptacle_USB2.0_16P" in bom
assert "RJ45" in bom and "PoE" in bom
assert "DDR3" not in bom and "DDR2" not in bom and "DDR4" not in bom
assert "USB3" not in bom
def test_rule_catalog_shared_not_si():
@@ -551,16 +553,14 @@ def test_rule_catalog_shared_not_si():
assert rec.domain == "shared"
def test_run_pcb_checks_includes_interface_class():
def test_run_pcb_checks_skips_absent_interfaces():
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)
assert "PE-USBC-001" not in ids
assert "PE-ETH-001" not in ids
assert "PE-POE-001" not in ids
if_f = [f for f in findings if (f.rule_id or "").startswith(("PE-USBC", "PE-ETH", "PE-POE", "PE-DDR"))]
assert if_f == []
def test_merge_drops_duplicate_pcb_interface_findings():
+1 -1
View File
@@ -48,7 +48,7 @@ def test_simple_project_without_pcb_has_no_ps_plc():
def test_simple_project_eval_has_no_placement_keys():
scores = eval_simple_project(SIMPLE)
assert scores.finding_count == 10
assert scores.finding_count == 8
assert scores.precision == 1.0
assert scores.recall == 1.0
assert not any(k.startswith("PE-PLC-") for k in scores.extra_keys)
+1 -1
View File
@@ -54,7 +54,7 @@ def test_simple_project_without_pcb_has_no_ps_si_001():
def test_simple_project_eval_has_no_si_keys():
scores = eval_simple_project(SIMPLE)
assert scores.finding_count == 10
assert scores.finding_count == 8
assert scores.precision == 1.0
assert scores.recall == 1.0
assert not any(
+2 -2
View File
@@ -160,9 +160,9 @@ async def test_concurrency_bounded_by_knob_and_charging_isolated(workspace, monk
assert review_n == len(IC_MPNS) # all 5 reviewed
if_ids = {
f.get("rule_id") for f in report["findings"]
if (f.get("rule_id") or "").startswith(("PE-USBC", "PE-ETH", "PE-POE"))
if (f.get("rule_id") or "").startswith(("PE-USBC", "PE-ETH", "PE-POE", "PE-DDR"))
}
assert "PE-USBC-001" in if_ids # N/A certifier, not a 6th IC review
assert not if_ids # five ICs, no USB-C/RJ45/DDR — do not invent those certifiers
@pytest.mark.asyncio