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
@@ -10,8 +10,6 @@
"PE-USBC-002|J1|/USBC.CC1",
"PE-USBC-003|J1|",
"PE-USBC-004|J1|",
"PE-USBC-005|J1|",
"PE-ETH-001|PCB|",
"PE-POE-001|PCB|"
"PE-USBC-005|J1|"
]
}
@@ -219,7 +219,7 @@ def _seed() -> None:
_add("PE-ETH-004", "RECOMMENDED", "REVIEW", domain="shared",
requirement="Bob Smith 75 Ω is typical for GbE; often inside MagJack.")
_add("PE-POE-001", "TYPICAL", "INFO", domain="shared",
requirement="PoE certifier runs only with PoE evidence; bare RJ45 is N/A.")
requirement="PoE certifier runs only with PoE evidence; skip a bare RJ45.")
_add("PE-POE-002", "TYPICAL", "INFO", domain="shared",
requirement="PoE class/type from 802.3af/at/bt or Class n — never invented.")
_add("PE-POE-003", "MANDATORY", "RULE", domain="shared",
@@ -67,7 +67,11 @@ _OHM_SUFFIX = re.compile(r"^(\d*\.?\d+)([RKMG])$", re.I)
def check_interface_classes(graph: DesignGraph) -> list[Finding]:
"""USB-C, Ethernet, and PoE class + connection integrity."""
"""Certify class + connection integrity for connectors actually on this graph.
USB-C, RJ45/Ethernet, PoE (only with PoE evidence) are the method — not a
catalog. No connector → no that certifier. No invented DDR/PoE/USB3/I/Z.
"""
out: list[Finding] = []
out.extend(_check_usb_c(graph))
out.extend(_check_ethernet(graph))
@@ -79,9 +83,9 @@ def format_interface_class_context(graph: DesignGraph) -> str:
"""Plain FACT block for AI review — not the verification engine."""
lines = [
"### Interface class (deterministic certifiers — do not invent)",
"USB-C: CC1/CC2, Rp/Rd 5.1 kΩ, VBUS/GND, SuperSpeed only if class is USB3.",
"Ethernet: 10/100 vs GbE; magnetics required for GbE. Bob Smith is recommended.",
"PoE: only with PoE evidence. Never invent PoE on a bare RJ45. No Z/I/mm.",
"USB-C / RJ45 / PoE only if that connector is on THIS graph. "
"PoE only with PoE evidence — never because RJ45 exists. "
"No DDR3 unless a DDR device is present. No invented Z/I/mm.",
]
findings = check_interface_classes(graph)
if not findings:
@@ -98,8 +102,7 @@ def format_interface_class_context(graph: DesignGraph) -> str:
def _check_usb_c(graph: DesignGraph) -> list[Finding]:
connectors = [c for c in graph.components.values() if _is_usb_c(c)]
if not connectors:
return [_na("PCB", "PE-USBC-001", "USB-C",
"No USB-C receptacle in the netlist (footprint/value/MPN/nets).")]
return []
out: list[Finding] = []
for comp in sorted(connectors, key=lambda c: c.reference):
out.extend(_usb_c_one(graph, comp))
@@ -248,8 +251,7 @@ def _usb_c_ss_finding(
def _check_ethernet(graph: DesignGraph) -> list[Finding]:
jacks = [c for c in graph.components.values() if _is_ethernet_jack(c)]
if not jacks:
return [_na("PCB", "PE-ETH-001", "Ethernet",
"No RJ45 / Ethernet jack in the netlist.")]
return []
out: list[Finding] = []
for comp in sorted(jacks, key=lambda c: c.reference):
out.extend(_eth_one(graph, comp))
@@ -377,15 +379,11 @@ def _eth_bob_smith_finding(
def _check_poe(graph: DesignGraph) -> list[Finding]:
jacks = [c for c in graph.components.values() if _is_ethernet_jack(c)]
if not jacks:
return [_na("PCB", "PE-POE-001", "PoE",
"No RJ45 — PoE is N/A (not invented).")]
return []
out: list[Finding] = []
for comp in sorted(jacks, key=lambda c: c.reference):
ev = _poe_blob(graph, comp)
if not ev:
out.append(_na(comp.reference, "PE-POE-001", "PoE",
f"{comp.reference} is RJ45 without PoE evidence — "
"PoE not certified (bare jack is not PoE)."))
continue
out.extend(_poe_one(graph, comp, ev))
return out
@@ -722,15 +720,6 @@ def _nets_equal(a: str, b: str) -> bool:
return _leaf(a).upper() == _leaf(b).upper()
def _na(designator: str, rule_id: str, kind: str, facts: str) -> Finding:
return _info(
designator, rule_id,
f"{kind} N/A — no candidate to certify.",
facts,
f"{kind} certifier runs always; absence is N/A, not ERROR.",
)
def _info(
designator: str, rule_id: str, finding: str, facts: str, requirement: str,
*, mpn: str = "", net: str | None = None, pins: list[str] | None = None,
@@ -36,11 +36,13 @@ notes. Do not claim the via count or size is missing if that block lists them.
Do not claim antenna keepout geometry is missing if keepout zones are listed.
- Kelvin/sense pins, crystal keepout — only if named in the pintable/layout_rules.
- Creepage/clearance only if the extraction or IEC number is in context.
- Interface class: USB-C CC/Rd/VBUS, Ethernet 10/100 vs GbE magnetics, \
PoE only with evidence. Deterministic certifiers already ran; explain \
those FACTS. Never invent PoE on a bare RJ45 or SuperSpeed on USB2 Type-C. \
Never invent millimetres, Z, or I. Do not assume USB 500 mA — PE-PWR \
skips when the datasheet has no I_load/Imax/I_abs.
- Interface class: only connectors on THIS board. USB-C CC/Rd/VBUS if a \
USB-C receptacle exists; Ethernet 10/100 vs GbE if RJ45 exists; PoE only \
with PoE evidence (never because RJ45 exists); DDR only if a DDR device \
is present. Deterministic certifiers already ran; explain those FACTS. \
Never invent a missing interface, PoE on a bare RJ45, or SuperSpeed on \
USB2 Type-C. Never invent millimetres, Z, or I. Do not assume USB 500 mA \
— PE-PWR skips when the datasheet has no I_load/Imax/I_abs.
### Evidence
Cite numbers from "Parsed board geometry". Say insufficient evidence ONLY \
@@ -40,10 +40,11 @@ datasheet's recommended values.
- Required external components named by the datasheet (bootstrap, \
compensation, feedback divider, sense resistor).
- Unused / no-connect pins.
- Dedicated interface class (USB-C CC1/CC2 Rp/Rd 5.1 kΩ VBUS/GND; Ethernet \
10/100 vs GbE magnetics; PoE only with evidence). Deterministic certifiers \
own those FACTS. Do not invent PoE on a bare RJ45, SuperSpeed on USB 2.0 \
Type-C, or geometry/Z/I. Do not assume USB 500 mA.
- Dedicated interface class only for connectors on THIS netlist (USB-C \
CC1/CC2 Rp/Rd 5.1 kΩ VBUS/GND; Ethernet 10/100 vs GbE magnetics; PoE only \
with evidence; DDR only if a DDR part exists). Deterministic certifiers \
own those FACTS. Do not invent a missing interface, PoE on a bare RJ45, \
SuperSpeed on USB 2.0 Type-C, or geometry/Z/I. Do not assume USB 500 mA.
Then work the areas one at a time. For EACH area, don't just confirm a \
part is present — ask what specific failure mode would make it wrong \
@@ -1,34 +1,24 @@
# Certifier di classe (interfacce)
Michele 2026-09-21: per USB-C, Ethernet (10/100 vs GbE), PoE, DDR3 e analoghe, check dedicati (deterministici + IA). Certificare **classe** e **integrità delle connessioni**. FEM termico: no, per ora.
Michele 2026-09-21: USB-C, RJ45/Ethernet, PoE, DDR3 sono **il metodo**, non un catalogo. Se **questa** scheda ha il connettore/bus, certifica classe + integrità delle connessioni. Se non c’è, non emettere quel certifier. HubAudio: J2 USB-C USB2 16P, J1 PoE 10/100 MagJack, niente DDR → niente finding DDR. RJ45 nudo ≠ PoE. FEM: no. I vs larghezza: solo con I datasheet (skip se manca).
## Cosa non è
Non è un unico check SI generico. Non è il DRC KiCad. Non è “via ampacity IPC” (traccia vs corrente è un altro motore).
Non è un unico check SI generico. Non è il DRC KiCad. Non è “via ampacity IPC”. Non è un elenco di bus immaginari.
## Formato finding
Classe dichiarata o inferita (FACT/REQUIREMENT da BOM, net, footprint, sheet). Se manca evidenza: INSUFFICIENT_EVIDENCE, niente geometria/Z/I inventati. Via ≠ pad ≠ track.
Classe dichiarata o inferita (FACT/REQUIREMENT da BOM, net, footprint, sheet). Se manca evidenza **sullinterfaccia presente**: INSUFFICIENT_EVIDENCE, niente geometria/Z/I inventati. Via ≠ pad ≠ track. Assenza di connettore → silenzio, non N/A.
## Prima fetta (shipped 2.61.0, skip-I 2.61.1)
## Presente su questa scheda (2.61.2)
| ID | Cosa certifica |
| --- | --- |
| PE-USBC-001 | Classe USB-C USB2 vs USB3 (BOM/footprint/MPN/net) |
| PE-USBC-002 | CC1 e CC2 connessi, non NC |
| PE-USBC-003 | Rd 5.1 kΩ ±10% verso GND, o Rp 56/22/10 kΩ verso VBUS; IC CC → INSUFFICIENT |
| PE-USBC-004 | VBUS e GND |
| PE-USBC-005 | Coppie SuperSpeed **solo** se la classe è USB3; USB2 = N/A |
| PE-ETH-001 | Classe 10/100 vs GbE |
| PE-ETH-002 | Coppie TX/RX (10/100) o quattro MDI (GbE) |
| PE-ETH-003 | Magnetics se GbE; N/A se non GbE |
| PE-ETH-004 | Bob Smith 75 Ω (RECOMMENDED; spesso nel MagJack) |
| PE-POE-001 | Evidenza PoE, altrimenti N/A (RJ45 nudo ≠ PoE) |
| PE-POE-002 | Classe/tipo 802.3af/at/bt se citati; altrimenti INSUFFICIENT |
| PE-POE-003 | Isolamento/magnetics solo con evidenza PoE |
| ID | Quando gira | Cosa certifica |
| --- | --- | --- |
| PE-USBC-001…005 | c’è un receptacle USB-C | classe USB2/USB3, CC1/CC2, Rd/Rp 5.1 kΩ, VBUS/GND, SuperSpeed solo se USB3 |
| PE-ETH-001…004 | c’è un RJ45 | 10/100 vs GbE, coppie TX/RX o 4 MDI, magnetics se GbE, Bob Smith REVIEW |
| PE-POE-001…003 | evidenza PoE sul jack | classe/tipo se citati; isolamento/magnetics; **skip** se RJ45 nudo |
| PE-DDR-* | solo se c’è un dispositivo DDR | non inventato su HubAudio (nessun DDR in BOM) |
Motore: `periscopex/interface_class_check.py` (grafo, non geometria). Schema `MODE=run` e PCB `MODE=pcb`. LIA spiega i FACT, non certifica. Nessun I inventato (niente USB 500 mA): la corrente vs larghezza è PE-PWR, e **skip** se il datasheet non riporta I_load/Imax/I_abs.
Motore: `periscopex/interface_class_check.py` (grafo, non geometria). Schema `MODE=run` e PCB `MODE=pcb`. LIA spiega i FACT, non certifica. Nessun I inventato: PE-PWR **skip** se il datasheet non riporta I_load/Imax/I_abs.
## Dopo
DDR3 (indirizzo/comando vs DQ, lunghezze, VTT/VREF). Altre periferiche con lo stesso schema.
HubAudio (parti vere): J2 `USB_C_Receptacle_USB2.0_16P` → USB-C USB2 (SS N/A); J1 ARJP11A PoE 10/100 MagJack → Ethernet 10/100 + PoE; J3/J4 fibra, J5 SMA, jack audio → non inventare bus. Niente DDR.
+2 -2
View File
@@ -76,7 +76,7 @@ Mutex vs analisi e vs placement. SSE `pcb_*`. IDs `PCB-{ref}-{001}`. KiCad `/net
| PE-STCH-001 | pour GND + segnale senza via GND nel bbox |
| PE-USBC-001…005 | classe USB-C + CC + Rd/Rp + VBUS/GND + SuperSpeed se USB3 |
| PE-ETH-001…004 | 10/100 vs GbE; magnetics se GbE; Bob Smith REVIEW |
| PE-POE-001…003 | PoE solo con evidenza; isolamento/magnetics; RJ45 nudo = N/A |
| PE-POE-001…003 | PoE **solo** con evidenza sul jack; RJ45 nudo = skip (non N/A) |
| Creepage | **skip** senza numero datasheet/IEC |
| Crystal keepout | PE-PLC-004 se kind=keepout |
@@ -125,7 +125,7 @@ Pintable skill **1.13.0** (`net_class` required on SI kinds). Older extracts wit
| Gap | Why it stays out |
| --- | --- |
| DDR3 class certifier | After USB-C / Ethernet / PoE first slice (2.61.0) |
| DDR class certifier | Only if a DDR device is on the graph. HubAudio has none — do not invent PE-DDR |
| Thermal FEM / energy | PE-THM-002 is Tj=Ta+P·θJA with measured copper/vias; no spreading/FEM |
| CPWG / OpenEMS | Field-solver export excluded from the ImpedenceFinder vendor snapshot |
| ImpedenceFinder license | Upstream has **no LICENSE** (UNKNOWN). Not invented in-tree. See `vendor/impedancefinder/SOURCE.md` |
@@ -2,6 +2,13 @@
What's new in Periscope.
## 2.61.2 — 2026-09-21 — Certifiers only for connectors on this board
USB-C / RJ45 / PoE / DDR3 are the method, not a catalog. If this graph has a USB-C receptacle, run USB-C questions. If it has RJ45, run Ethernet. PoE only with PoE evidence — never because RJ45 exists. No connector → no that certifier (not N/A). HubAudio has USB-C USB2 + PoE 10/100 MagJack and no DDR: no DDR findings. Skip-I (2.61.1) unchanged. No FEM, no via IPC, no invented I/Z/PoE/SuperSpeed.
- [Fixed] Absent USB-C / RJ45 / PoE no longer emit INFO N/A.
- [Fixed] Bare RJ45 is Ethernet only; PoE is skipped without PoE evidence.
## 2.61.1 — 2026-09-21 — Skip trace-width vs current without datasheet I
PE-PWR-001 (and PE-VIA-001) run only when the datasheet reports I_load / Imax / I_abs. Missing that number is a skip — not INFO INSUFFICIENT, not a typical USB 500 mA, not Iout_max-as-load. USB-C / Ethernet / PoE class certifiers still have no current/ampacity check. No via IPC table. No FEM.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "periscope-web",
"version": "2.61.1",
"version": "2.61.2",
"private": true,
"scripts": {
"sync-version": "node scripts/sync-version.mjs",
+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