Add protocol-certification M0 catalog, loader, and empty report (2.64.0).

Versioned JSON skeletons without invented ohms/mm/ps. RECOMMENDED is not
FAIL-mandatory; typical-as-standard is rejected. Report Protocolli is empty
until recognition (M1+). Native src overlays inherited periscopex on the
package path.
This commit is contained in:
2026-09-22 11:57:26 +02:00
parent c87adf11a9
commit 36ad979612
20 changed files with 5401 additions and 12 deletions
+5 -2
View File
@@ -16,8 +16,11 @@ for p in (
REPO_ROOT,
):
s = str(p)
if p.is_dir() and s not in sys.path:
sys.path.insert(0, s)
if not p.is_dir():
continue
if s in sys.path:
sys.path.remove(s)
sys.path.insert(0, s)
from backend.vendor_path import ensure_impedancefinder
+231
View File
@@ -0,0 +1,231 @@
"""M0 protocol pack: parse, reject typical-as-standard, no invented ohms."""
from __future__ import annotations
import copy
import json
import pytest
from backend.periscopex.models import ValidationReport
from backend.periscopex.protocol_catalog import (
CATALOG_PATH,
SCHEMA_PATH,
SCHEMA_VERSION,
ProtocolCatalogError,
build_m0_catalog,
empty_protocol_section,
fail_mandatory,
load_catalog,
map_protocol_outcome,
parse_catalog,
)
REQUIRED_LOGICAL = {
"usb2-ls-fs", "usb2-hs", "usb-c-usb2", "100base-tx",
"hdmi-1.4", "hdmi-2.0",
"ddr-family", "ddr4", "lpddr-family", "lpddr4",
"hbm-family", "hbm3", "hyperbus", "qspi", "octal-spi", "emmc", "ufs",
"axi4", "axi4-lite", "ahb", "apb", "wishbone", "avalon-mm", "avalon-st",
"tilelink", "pcie", "cxl",
}
def test_committed_catalog_parses_and_matches_builder():
loaded = load_catalog()
rebuilt = parse_catalog(build_m0_catalog())
assert loaded.schema_version == SCHEMA_VERSION
assert loaded.model_dump() == rebuilt.model_dump()
assert CATALOG_PATH.is_file()
assert SCHEMA_PATH.is_file()
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
assert "properties" in schema
assert schema["properties"]["schema_version"]
def test_logical_and_physical_ids_are_disjoint():
cat = load_catalog()
logical = {p.id for p in cat.logical_protocols}
physical = {p.id for p in cat.physical_interfaces}
assert not (logical & physical)
assert REQUIRED_LOGICAL <= logical
for iface in cat.physical_interfaces:
assert iface.id != iface.logical_protocol_id
assert iface.pcb_relevant in {"YES", "NO", "CONDITIONAL"}
def test_amba_internal_is_not_pcb_routing():
cat = load_catalog()
axi = next(p for p in cat.physical_interfaces if p.id == "axi4-internal")
assert axi.pcb_relevant == "NO"
assert axi.physical_layer == "INTERNAL"
assert axi.logical_protocol_id == "axi4"
assert axi.constraints[0].value_kind == "NOT_APPLICABLE"
def test_ddr_has_no_universal_mm_or_ps():
cat = load_catalog()
ddr4 = next(p for p in cat.physical_interfaces if p.id == "ddr4-sdram-pcb")
assert ddr4.pcb_relevant == "YES"
for c in ddr4.constraints:
assert c.value_kind in {
"CONTROLLER_DEPENDENT", "PHY_DEPENDENT", "UNKNOWN",
"VENDOR_DEPENDENT", "MISSING_SOURCE", "NOT_APPLICABLE",
}
assert c.value is None
assert c.unit is None
def test_hbm_dq_not_pcb_relevant():
cat = load_catalog()
hbm = next(p for p in cat.physical_interfaces if p.id == "hbm3-package")
assert hbm.pcb_relevant == "NO"
def test_hdmi_mini_is_same_logical_different_connector():
cat = load_catalog()
a = next(p for p in cat.physical_interfaces if p.id == "hdmi-1.4-tmds-type-a")
mini = next(
p for p in cat.physical_interfaces if p.id == "hdmi-1.4-tmds-type-c-mini"
)
assert a.logical_protocol_id == mini.logical_protocol_id == "hdmi-1.4"
assert a.connector == "HDMI-A"
assert mini.connector == "HDMI-C-mini"
def test_catalog_has_no_invented_ohm_numbers():
cat = load_catalog()
for iface in cat.physical_interfaces:
for c in iface.constraints:
assert c.value is None
assert c.value_kind != "NUMERIC"
def test_recommended_is_not_fail_mandatory():
cat = load_catalog()
recs = [
c
for iface in cat.physical_interfaces
for c in iface.constraints
if c.mandatory == "RECOMMENDED"
]
assert recs, "catalog should include at least one RECOMMENDED skeleton"
for c in recs:
assert fail_mandatory(c) is False
cls, status, _ = map_protocol_outcome("FAIL", c.mandatory)
assert status != "ERROR"
assert cls != "RULE"
def test_mandatory_fail_can_be_error():
cls, status, _ = map_protocol_outcome("FAIL", "MANDATORY")
assert cls == "RULE"
assert status == "ERROR"
def test_reject_typical_as_standard():
raw = build_m0_catalog()
iface = raw["physical_interfaces"][0]
typical = copy.deepcopy(iface["constraints"][0])
typical["id"] = "bad-typical"
typical["typical"] = True
typical["value_origin"] = "TYPICAL"
typical["source_type"] = "STANDARD"
typical["source_class"] = "NORMATIVE"
typical["mandatory"] = "MANDATORY"
typical["value_kind"] = "UNKNOWN"
typical["value"] = None
iface = copy.deepcopy(iface)
iface["id"] = "usb2-hs-typical-bad"
iface["constraints"] = [typical]
raw["physical_interfaces"].append(iface)
with pytest.raises(ProtocolCatalogError, match="typical"):
parse_catalog(raw)
def test_reject_constraint_without_source_type():
raw = build_m0_catalog()
iface = copy.deepcopy(raw["physical_interfaces"][0])
iface["id"] = "usb2-hs-no-source"
bad = copy.deepcopy(iface["constraints"][0])
bad["id"] = "no-source"
del bad["source_type"]
iface["constraints"] = [bad]
raw["physical_interfaces"].append(iface)
with pytest.raises(ProtocolCatalogError):
parse_catalog(raw)
def test_reject_numeric_without_cite():
raw = build_m0_catalog()
iface = copy.deepcopy(raw["physical_interfaces"][0])
iface["id"] = "usb2-hs-fake-z"
iface["constraints"] = [{
"id": "fake-zdiff",
"parameter": "differential_impedance",
"value_kind": "NUMERIC",
"value": 90.0,
"unit": "ohm",
"mandatory": "MANDATORY",
"source_type": "STANDARD",
"source_class": "NORMATIVE",
"source": None,
"typical": False,
"value_origin": "SPEC",
"conditions": [],
}]
raw["physical_interfaces"].append(iface)
with pytest.raises(ProtocolCatalogError, match="cite"):
parse_catalog(raw)
def test_reject_boolean_pcb_relevant():
raw = build_m0_catalog()
iface = copy.deepcopy(raw["physical_interfaces"][0])
iface["id"] = "usb2-hs-bool-pcb"
iface["pcb_relevant"] = True
raw["physical_interfaces"].append(iface)
with pytest.raises(ProtocolCatalogError):
parse_catalog(raw)
def test_reject_same_logical_and_physical_id():
raw = build_m0_catalog()
iface = copy.deepcopy(raw["physical_interfaces"][0])
iface["id"] = iface["logical_protocol_id"]
raw["physical_interfaces"].append(iface)
with pytest.raises(ProtocolCatalogError, match="differ"):
parse_catalog(raw)
def test_empty_report_section_has_no_z():
section = empty_protocol_section()
dumped = section.model_dump()
assert section.message == "nessun protocollo riconosciuto"
assert section.recognized_instances == []
assert dumped.get("max_level_reached") is None
blob = json.dumps(dumped)
assert "90" not in blob
assert "ohm" not in blob.lower()
assert "Zdiff" not in blob
assert "z0" not in blob.lower()
report = ValidationReport(
project="t",
timestamp="2026-09-22T00:00:00Z",
findings=[],
summary={"ERROR": 0, "WARNING": 0, "INFO": 0},
protocol_certification=dumped,
)
assert report.protocol_certification["message"] == "nessun protocollo riconosciuto"
def test_pcie_cxl_physical_packs_are_empty_numbers():
cat = load_catalog()
for pid in ("pcie-phy-pcb", "cxl-phy-pcb"):
iface = next(p for p in cat.physical_interfaces if p.id == pid)
assert iface.logical_protocol_id in {"pcie", "cxl"}
assert iface.required_checks
for c in iface.constraints:
assert c.value_kind in {"MISSING_SOURCE", "UNKNOWN"}
assert c.value is None