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
+24 -4
View File
@@ -10,9 +10,29 @@ from pathlib import Path
from pkgutil import extend_path
_REPO = Path(__file__).resolve().parents[1]
for _p in (_REPO / "periscope" / "src", _REPO / "periscope" / "dependency"):
_SRC = _REPO / "periscope" / "src"
_DEP = _REPO / "periscope" / "dependency"
# Last insert is searched first: native src overlays inherited dependency.
for _p in (_DEP, _SRC):
_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)
__path__ = list(extend_path(__path__, __name__))
def _prefer_native_backend(paths: list[str]) -> list[str]:
src, other, dep = [], [], []
for p in paths:
norm = p.replace("\\", "/")
if "/periscope/src/" in norm:
src.append(p)
elif "/periscope/dependency/" in norm:
dep.append(p)
else:
other.append(p)
return src + other + dep
__path__ = _prefer_native_backend(list(extend_path(__path__, __name__)))
+11 -1
View File
@@ -1,4 +1,14 @@
"""PinScope-inherited backend package (in-tree dependency)."""
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
__path__ = list(extend_path(__path__, __name__))
_src, _other, _dep = [], [], []
for _p in __path__:
_n = str(_p).replace("\\", "/")
if "/periscope/src/" in _n:
_src.append(_p)
elif "/periscope/dependency/" in _n:
_dep.append(_p)
else:
_other.append(_p)
__path__[:] = _src + _other + _dep
@@ -1,3 +1,13 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
__path__ = list(extend_path(__path__, __name__))
_src, _other, _dep = [], [], []
for _p in __path__:
_n = str(_p).replace("\\", "/")
if "/periscope/src/" in _n:
_src.append(_p)
elif "/periscope/dependency/" in _n:
_dep.append(_p)
else:
_other.append(_p)
__path__[:] = _src + _other + _dep
@@ -380,6 +380,7 @@ class ValidationReport(BaseModel):
coverage: dict[str, list[str]] = {} # designator -> areas checked and found OK
review_errors: dict[str, str] = {} # designator -> error message for ICs whose review raised
not_reviewed: list[dict] = [] # [{"designator","reason"}] — ICs skipped (e.g. no datasheet PDF)
protocol_certification: dict[str, Any] | None = None
class FindingComment(BaseModel):
+11 -1
View File
@@ -1,4 +1,14 @@
"""Native Periscope backend modules."""
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
__path__ = list(extend_path(__path__, __name__))
_src, _other, _dep = [], [], []
for _p in __path__:
_n = str(_p).replace("\\", "/")
if "/periscope/src/" in _n:
_src.append(_p)
elif "/periscope/dependency/" in _n:
_dep.append(_p)
else:
_other.append(_p)
__path__[:] = _src + _other + _dep
+11 -1
View File
@@ -1,4 +1,14 @@
"""Native Periscope core modules (finding engine, PCB, checks)."""
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)
__path__ = list(extend_path(__path__, __name__))
_src, _other, _dep = [], [], []
for _p in __path__:
_n = _p.replace("\\", "/")
if "/periscope/src/" in _n:
_src.append(_p)
elif "/periscope/dependency/" in _n:
_dep.append(_p)
else:
_other.append(_p)
__path__[:] = _src + _other + _dep
@@ -385,6 +385,8 @@ class ValidationReport(BaseModel):
coverage: dict[str, list[str]] = {} # designator -> areas checked and found OK
review_errors: dict[str, str] = {} # designator -> error message for ICs whose review raised
not_reviewed: list[dict] = [] # [{"designator","reason"}] — ICs skipped (e.g. no datasheet PDF)
# M0 protocol cert: empty instances / "nessun protocollo riconosciuto". No Z.
protocol_certification: dict[str, Any] | None = None
class FindingComment(BaseModel):
@@ -0,0 +1,781 @@
"""M0 protocol catalog: versioned JSON pack, loader, validator.
No invented ohms/mm/ps. Constraints without an official cite stay
UNKNOWN / CONTROLLER_DEPENDENT / PHY_DEPENDENT / MISSING_SOURCE /
VENDOR_DEPENDENT / NOT_APPLICABLE. logical_protocol_id is never the
same string as physical_interface_id. pcb_relevant is YES|NO|CONDITIONAL.
RECOMMENDED is not FAIL-mandatory. Typical-as-standard is rejected.
Recognition and L0L3 certifiers are M1+ — this module only loads packs
and builds the empty report section.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
SCHEMA_VERSION = "1.0.0"
PACK_MACROPHASE = "M0"
CATALOG_DIR = Path(__file__).resolve().parent / "protocol_data"
CATALOG_PATH = CATALOG_DIR / "catalog.json"
SCHEMA_PATH = CATALOG_DIR / f"schema-{SCHEMA_VERSION}.json"
EMPTY_PROTOCOL_MESSAGE = "nessun protocollo riconosciuto"
BusType = Literal[
"MEMORY",
"PROCESSOR_INTERCONNECT",
"PERIPHERAL",
"CHIP_TO_CHIP",
"STORAGE",
"GRAPHICS",
"SERIAL",
"PARALLEL",
]
PhysicalLayer = Literal[
"INTERNAL",
"PARALLEL_SINGLE_ENDED",
"PARALLEL_DIFFERENTIAL",
"SERIAL_SINGLE_ENDED",
"SERIAL_DIFFERENTIAL",
"MIXED",
]
PcbRelevant = Literal["YES", "NO", "CONDITIONAL"]
ValueKind = Literal[
"NUMERIC",
"UNKNOWN",
"CONTROLLER_DEPENDENT",
"PHY_DEPENDENT",
"VENDOR_DEPENDENT",
"NOT_APPLICABLE",
"MISSING_SOURCE",
]
MandatoryClass = Literal["MANDATORY", "RECOMMENDED", "OPTIONAL", "INFORMATIONAL"]
SourceType = Literal[
"STANDARD",
"CONNECTOR",
"PHY",
"CONTROLLER",
"MEMORY",
"COMPONENT",
"CABLE",
"PCB",
"DERIVED",
]
SourceClass = Literal["NORMATIVE", "VENDOR", "IMPLEMENTATION", "DERIVED"]
ValueOrigin = Literal["SPEC", "TYPICAL", "EXAMPLE", "DERIVED"]
MeasurementMethod = Literal[
"DATASHEET",
"TDR",
"FIELD_SOLVER",
"PCB_STACKUP",
"FABRICATOR",
"SIMULATION",
"CALCULATION",
"EXTRACTION",
"MEASUREMENT",
]
LimitKind = Literal["STANDARD", "DESIGN"]
ProtocolResult = Literal[
"PASS",
"FAIL",
"WARNING",
"UNKNOWN",
"NOT_APPLICABLE",
"VENDOR_DEPENDENT",
"MISSING_SOURCE",
]
class ProtocolCatalogError(ValueError):
"""Invalid protocol pack (typical-as-standard, missing source, …)."""
class ConstraintSource(BaseModel):
document: str | None = None
organization: str | None = None
revision: str | None = None
section: str | None = None
table: str | None = None
page: str | None = None
url: str | None = None
class ProtocolConstraint(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str
parameter: str
value_kind: ValueKind
mandatory: MandatoryClass
source_type: SourceType
source_class: SourceClass
value: float | None = None
unit: str | None = None
source: ConstraintSource | None = None
conditions: list[str] = Field(default_factory=list)
measurement_method: MeasurementMethod | None = None
limit_kind: LimitKind | None = None
value_origin: ValueOrigin = "SPEC"
typical: bool = False
@field_validator("source_type")
@classmethod
def _source_type_required(cls, v: str) -> str:
if not v:
raise ProtocolCatalogError("constraint without source_type")
return v
@model_validator(mode="after")
def _reject_typical_and_bare_numeric(self) -> ProtocolConstraint:
if self.typical or self.value_origin in {"TYPICAL", "EXAMPLE"}:
if self.source_type == "STANDARD" or self.source_class == "NORMATIVE":
raise ProtocolCatalogError(
f"{self.id}: typical/example cannot be STANDARD/NORMATIVE"
)
if self.mandatory == "MANDATORY":
raise ProtocolCatalogError(
f"{self.id}: typical/example cannot be MANDATORY"
)
if self.value_kind == "NUMERIC":
if self.value is None:
raise ProtocolCatalogError(f"{self.id}: NUMERIC needs a value")
if self.source is None or not (
self.source.document or self.source.organization
):
raise ProtocolCatalogError(
f"{self.id}: NUMERIC constraint needs an official cite"
)
elif self.value is not None:
raise ProtocolCatalogError(
f"{self.id}: non-NUMERIC value_kind cannot carry a number"
)
return self
def is_fail_mandatory(self) -> bool:
"""FAIL is allowed only for MANDATORY. RECOMMENDED never FAIL-mandatory."""
return self.mandatory == "MANDATORY"
class LogicalProtocol(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str
name: str
bus_type: BusType
family: str = ""
notes: str = ""
class PhysicalInterface(BaseModel):
model_config = ConfigDict(extra="forbid")
id: str
logical_protocol_id: str
bus_type: BusType
physical_layer: PhysicalLayer
pcb_relevant: PcbRelevant
connector: str = "none"
recognition_hints: list[str] = Field(default_factory=list)
required_checks: list[str] = Field(default_factory=list)
constraints: list[ProtocolConstraint] = Field(default_factory=list)
notes: str = ""
@model_validator(mode="after")
def _logical_differs_from_physical(self) -> PhysicalInterface:
if self.id == self.logical_protocol_id:
raise ProtocolCatalogError(
f"{self.id}: logical_protocol_id must differ from physical_interface_id"
)
return self
class ProtocolCatalog(BaseModel):
model_config = ConfigDict(extra="forbid")
schema_version: str
logical_protocols: list[LogicalProtocol]
physical_interfaces: list[PhysicalInterface]
@model_validator(mode="after")
def _index_integrity(self) -> ProtocolCatalog:
if self.schema_version != SCHEMA_VERSION:
raise ProtocolCatalogError(
f"schema_version {self.schema_version!r} != {SCHEMA_VERSION}"
)
logical_ids = [p.id for p in self.logical_protocols]
if len(logical_ids) != len(set(logical_ids)):
raise ProtocolCatalogError("duplicate logical_protocol id")
physical_ids = [p.id for p in self.physical_interfaces]
if len(physical_ids) != len(set(physical_ids)):
raise ProtocolCatalogError("duplicate physical_interface id")
logical_set = set(logical_ids)
overlap = logical_set & set(physical_ids)
if overlap:
raise ProtocolCatalogError(
f"id used as both logical and physical: {sorted(overlap)}"
)
for iface in self.physical_interfaces:
if iface.logical_protocol_id not in logical_set:
raise ProtocolCatalogError(
f"{iface.id}: unknown logical_protocol_id "
f"{iface.logical_protocol_id}"
)
return self
class ProtocolCertificationSection(BaseModel):
"""Report hook. M0: no instances, no Z numbers."""
schema_version: str = SCHEMA_VERSION
macrophase: str = PACK_MACROPHASE
recognized_instances: list[dict[str, Any]] = Field(default_factory=list)
message: str = EMPTY_PROTOCOL_MESSAGE
max_level_reached: str | None = None
def empty_protocol_section() -> ProtocolCertificationSection:
return ProtocolCertificationSection()
def fail_mandatory(constraint: ProtocolConstraint) -> bool:
return constraint.is_fail_mandatory()
def map_protocol_outcome(
result: ProtocolResult,
mandatory: MandatoryClass,
) -> tuple[str, str, str]:
"""(finding_class, status, evidence_status) — RECOMMENDED never ERROR FAIL."""
if result == "PASS":
return "RULE", "INFO", "SUFFICIENT"
if result == "NOT_APPLICABLE":
return "INFO", "INFO", "SUFFICIENT"
if result in {"UNKNOWN", "MISSING_SOURCE"}:
return "REVIEW", "WARNING", "INSUFFICIENT"
if result == "VENDOR_DEPENDENT":
return "REVIEW", "WARNING", "INSUFFICIENT"
if result == "FAIL":
if mandatory != "MANDATORY":
return "RISK", "WARNING", "SUFFICIENT"
return "RULE", "ERROR", "SUFFICIENT"
if result == "WARNING":
return "RISK", "WARNING", "SUFFICIENT"
return "REVIEW", "WARNING", "INSUFFICIENT"
def catalog_json_schema() -> dict[str, Any]:
return ProtocolCatalog.model_json_schema()
def load_catalog(path: Path | None = None) -> ProtocolCatalog:
target = path or CATALOG_PATH
raw = json.loads(target.read_text(encoding="utf-8"))
return parse_catalog(raw)
def parse_catalog(raw: Any) -> ProtocolCatalog:
if not isinstance(raw, dict):
raise ProtocolCatalogError("catalog must be a JSON object")
try:
return ProtocolCatalog.model_validate(raw)
except ProtocolCatalogError:
raise
except Exception as exc:
raise ProtocolCatalogError(str(exc)) from exc
def _c(
iface_id: str,
parameter: str,
value_kind: ValueKind,
*,
mandatory: MandatoryClass = "MANDATORY",
source_type: SourceType = "STANDARD",
source_class: SourceClass = "NORMATIVE",
conditions: list[str] | None = None,
measurement_method: MeasurementMethod | None = None,
) -> dict[str, Any]:
row: dict[str, Any] = {
"id": f"{iface_id}-{parameter}",
"parameter": parameter,
"value_kind": value_kind,
"mandatory": mandatory,
"source_type": source_type,
"source_class": source_class,
"value": None,
"unit": None,
"source": None,
"conditions": conditions or [],
"value_origin": "SPEC",
"typical": False,
}
if measurement_method:
row["measurement_method"] = measurement_method
return row
def _iface(
pid: str,
logical: str,
bus_type: BusType,
layer: PhysicalLayer,
pcb: PcbRelevant,
checks: list[str],
constraints: list[dict[str, Any]],
*,
connector: str = "none",
hints: list[str] | None = None,
notes: str = "",
) -> dict[str, Any]:
return {
"id": pid,
"logical_protocol_id": logical,
"bus_type": bus_type,
"physical_layer": layer,
"pcb_relevant": pcb,
"connector": connector,
"recognition_hints": hints or [],
"required_checks": checks,
"constraints": constraints,
"notes": notes,
}
USB2_CHECKS = [
"differential_impedance",
"intra_pair_skew",
"topology",
"length",
"vias",
"return_path",
]
ETH_CHECKS = [
"differential_impedance",
"pair_skew",
"length",
"magnetics",
"return_path",
"phy_requirements",
]
HDMI_CHECKS = [
"differential_impedance",
"pair_skew",
"lane_relationships",
"insertion_loss",
"return_loss",
"vias",
"connector",
]
DDR_CHECKS = [
"byte_lane_mapping",
"dq_to_dqs_skew",
"byte_lane_skew",
"ck_to_command_skew",
"topology",
"impedance",
"termination",
"via_count",
"stub_length",
"return_path",
]
QSPI_CHECKS = ["clock_length", "data_length", "stubs", "vias", "return_path"]
OCTAL_CHECKS = [
"dq_group",
"dqs_relationship_if_present",
"clock_to_data_skew",
"topology",
"impedance_if_required",
"length",
"vias",
"stubs",
]
EMMC_CHECKS = ["cmd_dat_grouping", "clk_reference", "impedance", "skew", "length"]
UFS_CHECKS = [
"differential_impedance",
"intra_pair_skew",
"insertion_loss",
"return_loss",
"via_transition",
"ac_coupling",
]
PCIE_CHECKS = [
"differential_impedance",
"intra_pair_skew",
"lane_to_lane_skew",
"insertion_loss",
"return_loss",
"vias",
"connectors",
"return_path",
]
HYPERBUS_CHECKS = [
"dq_to_rwds_skew",
"ck_to_dq_relationship",
"impedance",
"topology",
"length",
"vias",
"stubs",
]
def build_m0_catalog() -> dict[str, Any]:
"""Skeleton catalog. No numeric USB/ETH/HDMI/DDR values."""
logical: list[dict[str, Any]] = []
physical: list[dict[str, Any]] = []
def L(pid: str, name: str, bus: BusType, family: str, notes: str = "") -> None:
logical.append({
"id": pid, "name": name, "bus_type": bus, "family": family, "notes": notes,
})
L("usb2-ls-fs", "USB 2.0 Low/Full Speed", "SERIAL", "USB")
L("usb2-hs", "USB 2.0 High Speed", "SERIAL", "USB")
L("usb-c-usb2", "USB Type-C carrying USB 2.x only", "SERIAL", "USB")
L("usb-c-pd", "USB Power Delivery on CC", "SERIAL", "USB")
L("100base-tx", "100BASE-TX Fast Ethernet", "SERIAL", "ETHERNET")
L("hdmi-1.4", "HDMI 1.4 TMDS", "SERIAL", "HDMI")
L("hdmi-2.0", "HDMI 2.0 TMDS", "SERIAL", "HDMI")
L("ddr-family", "DDR family (abstract)", "MEMORY", "DDR")
for gen, title in (
("ddr1", "DDR1"), ("ddr2", "DDR2"), ("ddr3", "DDR3"),
("ddr3l", "DDR3L"), ("ddr4", "DDR4"), ("ddr5", "DDR5"),
):
L(gen, title, "MEMORY", "DDR")
L("lpddr-family", "LPDDR family (abstract)", "MEMORY", "LPDDR")
for gen, title in (
("lpddr2", "LPDDR2"), ("lpddr3", "LPDDR3"), ("lpddr4", "LPDDR4"),
("lpddr4x", "LPDDR4X"), ("lpddr5", "LPDDR5"), ("lpddr5x", "LPDDR5X"),
):
L(gen, title, "MEMORY", "LPDDR")
L("hbm-family", "HBM family (abstract)", "MEMORY", "HBM")
for gen, title in (
("hbm", "HBM"), ("hbm2", "HBM2"), ("hbm2e", "HBM2E"),
("hbm3", "HBM3"), ("hbm3e", "HBM3E"),
):
L(gen, title, "MEMORY", "HBM")
L("hyperbus", "HyperBus", "MEMORY", "HYPERBUS")
L("hyperram", "HyperRAM", "MEMORY", "HYPERBUS")
L("spi-nor", "SPI NOR", "MEMORY", "SPI")
L("qspi", "Quad SPI", "MEMORY", "SPI")
L("octal-spi", "Octal SPI", "MEMORY", "SPI")
L("opi", "OPI", "MEMORY", "SPI")
L("xspi", "xSPI", "MEMORY", "SPI")
L("emmc", "eMMC", "STORAGE", "STORAGE")
L("ufs", "UFS", "STORAGE", "STORAGE")
L("amba", "AMBA (umbrella)", "PROCESSOR_INTERCONNECT", "AMBA")
for pid, name in (
("axi4", "AXI4"), ("axi4-lite", "AXI4-Lite"), ("axi4-stream", "AXI4-Stream"),
("ahb", "AHB"), ("ahb-lite", "AHB-Lite"), ("apb", "APB"),
("ace", "ACE"), ("ace-lite", "ACE-Lite"), ("amba-chi", "AMBA CHI"),
):
L(pid, name, "PROCESSOR_INTERCONNECT", "AMBA")
L("wishbone", "Wishbone", "PROCESSOR_INTERCONNECT", "OPEN_BUS")
L("avalon-mm", "Avalon-MM", "PROCESSOR_INTERCONNECT", "INTEL_FPGA")
L("avalon-st", "Avalon-ST", "PROCESSOR_INTERCONNECT", "INTEL_FPGA")
L("tilelink", "TileLink", "PROCESSOR_INTERCONNECT", "RISC-V")
L("pcie", "PCI Express (logical)", "CHIP_TO_CHIP", "PCIE")
L("cxl", "CXL (logical)", "CHIP_TO_CHIP", "CXL")
missing_usb = [
_c("usb2-hs-dpair", "differential_impedance", "MISSING_SOURCE",
measurement_method="PCB_STACKUP"),
_c("usb2-hs-dpair", "intra_pair_skew", "MISSING_SOURCE",
measurement_method="CALCULATION"),
_c("usb2-hs-dpair", "topology", "UNKNOWN"),
_c("usb2-hs-dpair", "length", "UNKNOWN"),
_c("usb2-hs-dpair", "vias", "UNKNOWN"),
_c("usb2-hs-dpair", "return_path", "UNKNOWN", mandatory="RECOMMENDED",
source_class="VENDOR"),
]
physical.append(_iface(
"usb2-ls-fs-dpair", "usb2-ls-fs", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
USB2_CHECKS,
[
_c("usb2-ls-fs-dpair", "differential_impedance", "MISSING_SOURCE",
measurement_method="PCB_STACKUP"),
_c("usb2-ls-fs-dpair", "intra_pair_skew", "MISSING_SOURCE"),
_c("usb2-ls-fs-dpair", "topology", "UNKNOWN"),
],
hints=["USB_D+", "USB_D-", "USB_DP", "USB_DM", "D+", "D-"],
notes="LS/FS pair. No invented Z.",
))
physical.append(_iface(
"usb2-hs-dpair", "usb2-hs", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
USB2_CHECKS, missing_usb,
hints=["USB_D+", "USB_D-", "USB_HS"],
notes="HS pair. Official USB-IF cite required before NUMERIC Z.",
))
physical.append(_iface(
"usb-c-usb2-receptacle", "usb-c-usb2", "SERIAL", "MIXED", "YES",
USB2_CHECKS + ["cc_rd_rp", "vbus_gnd", "superspeed_absence"],
[
_c("usb-c-usb2-receptacle", "differential_impedance", "MISSING_SOURCE"),
_c("usb-c-usb2-receptacle", "cc_termination", "MISSING_SOURCE",
source_type="CONNECTOR", source_class="NORMATIVE"),
_c("usb-c-usb2-receptacle", "superspeed_pairs", "NOT_APPLICABLE",
mandatory="INFORMATIONAL",
conditions=["usb2_only"]),
],
connector="Type-C",
hints=["CC1", "CC2", "USB_C", "TYPE_C", "A5", "B5"],
notes="Type-C carrying USB2. Electrical USB2 + CC. No invented Rd ohms.",
))
physical.append(_iface(
"usb-c-pd-cc", "usb-c-pd", "SERIAL", "SERIAL_SINGLE_ENDED", "CONDITIONAL",
["cc_pd_contract"],
[_c("usb-c-pd-cc", "pd_contract", "MISSING_SOURCE",
source_type="STANDARD", conditions=["pd_present"])],
connector="Type-C",
hints=["CC1", "CC2", "USB_PD"],
))
physical.append(_iface(
"100base-tx-mdi", "100base-tx", "SERIAL", "SERIAL_DIFFERENTIAL", "YES",
ETH_CHECKS,
[
_c("100base-tx-mdi", "differential_impedance", "MISSING_SOURCE",
source_type="STANDARD"),
_c("100base-tx-mdi", "pair_skew", "MISSING_SOURCE"),
_c("100base-tx-mdi", "magnetics", "PHY_DEPENDENT",
source_type="PHY", source_class="VENDOR"),
_c("100base-tx-mdi", "bob_smith", "UNKNOWN", mandatory="RECOMMENDED",
source_type="PHY", source_class="VENDOR"),
],
connector="RJ45",
hints=["TD+", "TD-", "RD+", "RD-", "MDI", "RJ45", "100BASE"],
notes="IEEE numbers stay MISSING_SOURCE until the spec is on file.",
))
for hid, lid, conn, note in (
("hdmi-1.4-tmds-type-a", "hdmi-1.4", "HDMI-A", "Type A land"),
("hdmi-1.4-tmds-type-c-mini", "hdmi-1.4", "HDMI-C-mini",
"Mini Type C land; electrical HDMI 1.4, not a second standard"),
("hdmi-2.0-tmds-type-a", "hdmi-2.0", "HDMI-A", "Type A land"),
("hdmi-2.0-tmds-type-c-mini", "hdmi-2.0", "HDMI-C-mini",
"Mini Type C land; electrical HDMI 2.0"),
):
physical.append(_iface(
hid, lid, "SERIAL", "SERIAL_DIFFERENTIAL", "YES", HDMI_CHECKS,
[
_c(hid, "differential_impedance", "MISSING_SOURCE"),
_c(hid, "pair_skew", "MISSING_SOURCE"),
_c(hid, "insertion_loss", "MISSING_SOURCE", mandatory="RECOMMENDED"),
],
connector=conn, hints=["HDMI", "TMDS", "TX0", "TX1", "TX2", "CLK"],
notes=note,
))
def _mem_constraints(pid: str, kind: ValueKind, source_type: SourceType) -> list[dict[str, Any]]:
cls: SourceClass = "VENDOR" if source_type != "STANDARD" else "NORMATIVE"
params = [
"impedance", "max_length", "dq_to_dqs_skew", "byte_to_byte_skew",
"topology", "termination",
]
return [
_c(pid, p, kind, source_type=source_type, source_class=cls)
for p in params
]
physical.append(_iface(
"ddr-family-sdram-pcb", "ddr-family", "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
DDR_CHECKS, _mem_constraints("ddr-family-sdram-pcb", "CONTROLLER_DEPENDENT", "CONTROLLER"),
hints=["DQ", "DQS", "CK", "A0", "RAS", "CAS", "WE"],
notes="No universal mm/ps for DDR.",
))
for gen in ("ddr1", "ddr2", "ddr3", "ddr3l", "ddr4", "ddr5"):
kind: ValueKind = (
"PHY_DEPENDENT" if gen in {"ddr4", "ddr5"} else "CONTROLLER_DEPENDENT"
)
src: SourceType = "PHY" if kind == "PHY_DEPENDENT" else "CONTROLLER"
physical.append(_iface(
f"{gen}-sdram-pcb", gen, "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
DDR_CHECKS, _mem_constraints(f"{gen}-sdram-pcb", kind, src),
hints=["DQ", "DQS", "CK", gen.upper()],
notes="Controller/PHY dependent. Zero universal skew/length.",
))
physical.append(_iface(
"lpddr-family-pcb", "lpddr-family", "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
DDR_CHECKS, _mem_constraints("lpddr-family-pcb", "CONTROLLER_DEPENDENT", "CONTROLLER"),
hints=["DQ", "DQS", "CK", "CA", "LPDDR"],
))
for gen, kind in (
("lpddr2", "CONTROLLER_DEPENDENT"),
("lpddr3", "CONTROLLER_DEPENDENT"),
("lpddr4", "PHY_DEPENDENT"),
("lpddr4x", "PHY_DEPENDENT"),
("lpddr5", "PHY_DEPENDENT"),
("lpddr5x", "PHY_DEPENDENT"),
):
src = "PHY" if kind == "PHY_DEPENDENT" else "CONTROLLER"
physical.append(_iface(
f"{gen}-pcb", gen, "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
DDR_CHECKS, _mem_constraints(f"{gen}-pcb", kind, src), # type: ignore[arg-type]
hints=["DQ", "DQS", "CK", "CA", "WCK", gen.upper()],
))
for gen in ("hbm", "hbm2", "hbm2e", "hbm3", "hbm3e"):
physical.append(_iface(
f"{gen}-package", gen, "MEMORY", "INTERNAL", "NO",
["package", "silicon"],
[
_c(f"{gen}-package", "pcb_individual_dq_routing", "NOT_APPLICABLE",
mandatory="INFORMATIONAL", source_type="PCB", source_class="IMPLEMENTATION"),
],
notes="HBM is package/interposer. Do not apply DDR DQ PCB rules.",
))
physical.append(_iface(
"hbm-family-package", "hbm-family", "MEMORY", "INTERNAL", "NO",
["package", "silicon"],
[_c("hbm-family-package", "pcb_individual_dq_routing", "NOT_APPLICABLE",
mandatory="INFORMATIONAL", source_type="PCB", source_class="IMPLEMENTATION")],
))
physical.append(_iface(
"hyperbus-pcb", "hyperbus", "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
HYPERBUS_CHECKS,
[
_c("hyperbus-pcb", "dq_to_rwds_skew", "VENDOR_DEPENDENT",
source_type="COMPONENT", source_class="VENDOR"),
_c("hyperbus-pcb", "impedance", "VENDOR_DEPENDENT",
source_type="COMPONENT", source_class="VENDOR"),
],
hints=["RWDS", "DQ0", "CK", "HYPERBUS"],
))
physical.append(_iface(
"hyperram-pcb", "hyperram", "MEMORY", "PARALLEL_SINGLE_ENDED", "YES",
HYPERBUS_CHECKS,
[_c("hyperram-pcb", "impedance", "VENDOR_DEPENDENT",
source_type="COMPONENT", source_class="VENDOR")],
hints=["HYPERRAM", "RWDS"],
))
physical.append(_iface(
"spi-nor-pcb", "spi-nor", "MEMORY", "SERIAL_SINGLE_ENDED", "YES",
["topology", "length"],
[
_c("spi-nor-pcb", "topology", "UNKNOWN", mandatory="RECOMMENDED",
source_type="COMPONENT", source_class="VENDOR"),
_c("spi-nor-pcb", "impedance", "NOT_APPLICABLE", mandatory="INFORMATIONAL",
source_type="COMPONENT", source_class="VENDOR"),
_c("spi-nor-pcb", "length", "VENDOR_DEPENDENT",
source_type="COMPONENT", source_class="VENDOR"),
],
hints=["SCLK", "CS", "MOSI", "MISO", "SPI"],
))
physical.append(_iface(
"qspi-pcb", "qspi", "MEMORY", "SERIAL_SINGLE_ENDED", "YES", QSPI_CHECKS,
[
_c("qspi-pcb", "clock_length", "VENDOR_DEPENDENT",
source_type="COMPONENT", source_class="VENDOR"),
_c("qspi-pcb", "impedance", "UNKNOWN",
source_type="COMPONENT", source_class="VENDOR"),
],
hints=["QSPI", "IO0", "IO1", "IO2", "IO3", "SCLK"],
))
for pid, logical_id in (
("octal-spi-pcb", "octal-spi"), ("opi-pcb", "opi"), ("xspi-pcb", "xspi"),
):
physical.append(_iface(
pid, logical_id, "MEMORY", "SERIAL_SINGLE_ENDED", "YES", OCTAL_CHECKS,
[
_c(pid, "clock_to_data_skew", "VENDOR_DEPENDENT",
source_type="COMPONENT", source_class="VENDOR"),
_c(pid, "impedance", "UNKNOWN",
source_type="COMPONENT", source_class="VENDOR"),
],
hints=["DQ0", "DQS", "OSPI", "XSPI", "OCTAL"],
))
physical.append(_iface(
"emmc-pcb", "emmc", "STORAGE", "PARALLEL_SINGLE_ENDED", "YES", EMMC_CHECKS,
[
_c("emmc-pcb", "impedance", "VENDOR_DEPENDENT",
source_type="COMPONENT", source_class="VENDOR"),
_c("emmc-pcb", "skew", "UNKNOWN", conditions=["mode=HS200|HS400"]),
],
hints=["eMMC", "DAT0", "CMD", "MMC_CLK"],
))
physical.append(_iface(
"ufs-mphy-pcb", "ufs", "STORAGE", "SERIAL_DIFFERENTIAL", "YES", UFS_CHECKS,
[
_c("ufs-mphy-pcb", "differential_impedance", "MISSING_SOURCE"),
_c("ufs-mphy-pcb", "intra_pair_skew", "MISSING_SOURCE"),
],
hints=["UFS", "MPHY", "TX+", "RX+"],
))
amba = [
"axi4", "axi4-lite", "axi4-stream", "ahb", "ahb-lite", "apb",
"ace", "ace-lite", "amba-chi", "amba",
]
for pid in amba:
physical.append(_iface(
f"{pid}-internal", pid, "PROCESSOR_INTERCONNECT", "INTERNAL", "NO",
[],
[_c(f"{pid}-internal", "pcb_routing", "NOT_APPLICABLE",
mandatory="INFORMATIONAL", source_type="PCB",
source_class="IMPLEMENTATION")],
notes="On-chip interconnect. Recognisable; not PCB routing.",
))
physical.append(_iface(
"axi4-chip-to-chip-phy", "axi4", "CHIP_TO_CHIP", "MIXED", "CONDITIONAL",
["impedance", "timing", "topology", "voltage"],
[
_c("axi4-chip-to-chip-phy", "impedance", "PHY_DEPENDENT",
source_type="PHY", source_class="VENDOR"),
_c("axi4-chip-to-chip-phy", "timing", "VENDOR_DEPENDENT",
source_type="COMPONENT", source_class="VENDOR"),
],
notes="AXI4 name is logical. PCB rules come from the external PHY, not AXI4.",
))
for pid in ("wishbone", "avalon-mm", "avalon-st", "tilelink"):
physical.append(_iface(
f"{pid}-internal", pid, "PROCESSOR_INTERCONNECT", "INTERNAL", "NO",
[],
[_c(f"{pid}-internal", "pcb_routing", "NOT_APPLICABLE",
mandatory="INFORMATIONAL", source_type="PCB",
source_class="IMPLEMENTATION")],
))
physical.append(_iface(
"pcie-phy-pcb", "pcie", "CHIP_TO_CHIP", "SERIAL_DIFFERENTIAL", "YES",
PCIE_CHECKS,
[
_c("pcie-phy-pcb", "differential_impedance", "MISSING_SOURCE"),
_c("pcie-phy-pcb", "intra_pair_skew", "MISSING_SOURCE"),
_c("pcie-phy-pcb", "insertion_loss", "MISSING_SOURCE"),
],
hints=["PCIE", "PETX", "PERX", "REFCLK"],
notes="Logical vs physical split. Empty numbers until PCI-SIG cite.",
))
physical.append(_iface(
"cxl-phy-pcb", "cxl", "CHIP_TO_CHIP", "SERIAL_DIFFERENTIAL", "YES",
PCIE_CHECKS,
[
_c("cxl-phy-pcb", "differential_impedance", "MISSING_SOURCE"),
_c("cxl-phy-pcb", "intra_pair_skew", "MISSING_SOURCE"),
],
hints=["CXL"],
notes="Physical electrical pack empty until official cite.",
))
return {
"schema_version": SCHEMA_VERSION,
"logical_protocols": logical,
"physical_interfaces": physical,
}
def write_catalog_files(directory: Path | None = None) -> tuple[Path, Path]:
directory = directory or CATALOG_DIR
directory.mkdir(parents=True, exist_ok=True)
catalog_path = directory / "catalog.json"
schema_path = directory / f"schema-{SCHEMA_VERSION}.json"
data = build_m0_catalog()
parse_catalog(data)
catalog_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
schema_path.write_text(
json.dumps(catalog_json_schema(), indent=2) + "\n", encoding="utf-8",
)
return catalog_path, schema_path
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,422 @@
{
"$defs": {
"ConstraintSource": {
"properties": {
"document": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Document"
},
"organization": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Organization"
},
"revision": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Revision"
},
"section": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Section"
},
"table": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Table"
},
"page": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Page"
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Url"
}
},
"title": "ConstraintSource",
"type": "object"
},
"LogicalProtocol": {
"additionalProperties": false,
"properties": {
"id": {
"title": "Id",
"type": "string"
},
"name": {
"title": "Name",
"type": "string"
},
"bus_type": {
"enum": [
"MEMORY",
"PROCESSOR_INTERCONNECT",
"PERIPHERAL",
"CHIP_TO_CHIP",
"STORAGE",
"GRAPHICS",
"SERIAL",
"PARALLEL"
],
"title": "Bus Type",
"type": "string"
},
"family": {
"default": "",
"title": "Family",
"type": "string"
},
"notes": {
"default": "",
"title": "Notes",
"type": "string"
}
},
"required": [
"id",
"name",
"bus_type"
],
"title": "LogicalProtocol",
"type": "object"
},
"PhysicalInterface": {
"additionalProperties": false,
"properties": {
"id": {
"title": "Id",
"type": "string"
},
"logical_protocol_id": {
"title": "Logical Protocol Id",
"type": "string"
},
"bus_type": {
"enum": [
"MEMORY",
"PROCESSOR_INTERCONNECT",
"PERIPHERAL",
"CHIP_TO_CHIP",
"STORAGE",
"GRAPHICS",
"SERIAL",
"PARALLEL"
],
"title": "Bus Type",
"type": "string"
},
"physical_layer": {
"enum": [
"INTERNAL",
"PARALLEL_SINGLE_ENDED",
"PARALLEL_DIFFERENTIAL",
"SERIAL_SINGLE_ENDED",
"SERIAL_DIFFERENTIAL",
"MIXED"
],
"title": "Physical Layer",
"type": "string"
},
"pcb_relevant": {
"enum": [
"YES",
"NO",
"CONDITIONAL"
],
"title": "Pcb Relevant",
"type": "string"
},
"connector": {
"default": "none",
"title": "Connector",
"type": "string"
},
"recognition_hints": {
"items": {
"type": "string"
},
"title": "Recognition Hints",
"type": "array"
},
"required_checks": {
"items": {
"type": "string"
},
"title": "Required Checks",
"type": "array"
},
"constraints": {
"items": {
"$ref": "#/$defs/ProtocolConstraint"
},
"title": "Constraints",
"type": "array"
},
"notes": {
"default": "",
"title": "Notes",
"type": "string"
}
},
"required": [
"id",
"logical_protocol_id",
"bus_type",
"physical_layer",
"pcb_relevant"
],
"title": "PhysicalInterface",
"type": "object"
},
"ProtocolConstraint": {
"additionalProperties": false,
"properties": {
"id": {
"title": "Id",
"type": "string"
},
"parameter": {
"title": "Parameter",
"type": "string"
},
"value_kind": {
"enum": [
"NUMERIC",
"UNKNOWN",
"CONTROLLER_DEPENDENT",
"PHY_DEPENDENT",
"VENDOR_DEPENDENT",
"NOT_APPLICABLE",
"MISSING_SOURCE"
],
"title": "Value Kind",
"type": "string"
},
"mandatory": {
"enum": [
"MANDATORY",
"RECOMMENDED",
"OPTIONAL",
"INFORMATIONAL"
],
"title": "Mandatory",
"type": "string"
},
"source_type": {
"enum": [
"STANDARD",
"CONNECTOR",
"PHY",
"CONTROLLER",
"MEMORY",
"COMPONENT",
"CABLE",
"PCB",
"DERIVED"
],
"title": "Source Type",
"type": "string"
},
"source_class": {
"enum": [
"NORMATIVE",
"VENDOR",
"IMPLEMENTATION",
"DERIVED"
],
"title": "Source Class",
"type": "string"
},
"value": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"default": null,
"title": "Value"
},
"unit": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Unit"
},
"source": {
"anyOf": [
{
"$ref": "#/$defs/ConstraintSource"
},
{
"type": "null"
}
],
"default": null
},
"conditions": {
"items": {
"type": "string"
},
"title": "Conditions",
"type": "array"
},
"measurement_method": {
"anyOf": [
{
"enum": [
"DATASHEET",
"TDR",
"FIELD_SOLVER",
"PCB_STACKUP",
"FABRICATOR",
"SIMULATION",
"CALCULATION",
"EXTRACTION",
"MEASUREMENT"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Measurement Method"
},
"limit_kind": {
"anyOf": [
{
"enum": [
"STANDARD",
"DESIGN"
],
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Limit Kind"
},
"value_origin": {
"default": "SPEC",
"enum": [
"SPEC",
"TYPICAL",
"EXAMPLE",
"DERIVED"
],
"title": "Value Origin",
"type": "string"
},
"typical": {
"default": false,
"title": "Typical",
"type": "boolean"
}
},
"required": [
"id",
"parameter",
"value_kind",
"mandatory",
"source_type",
"source_class"
],
"title": "ProtocolConstraint",
"type": "object"
}
},
"additionalProperties": false,
"properties": {
"schema_version": {
"title": "Schema Version",
"type": "string"
},
"logical_protocols": {
"items": {
"$ref": "#/$defs/LogicalProtocol"
},
"title": "Logical Protocols",
"type": "array"
},
"physical_interfaces": {
"items": {
"$ref": "#/$defs/PhysicalInterface"
},
"title": "Physical Interfaces",
"type": "array"
}
},
"required": [
"schema_version",
"logical_protocols",
"physical_interfaces"
],
"title": "ProtocolCatalog",
"type": "object"
}
@@ -28,6 +28,7 @@ from backend.periscopex.layout_rules import needs_layout_rules_refresh
from backend.periscopex.models import (
ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, ValidationReport,
)
from backend.periscopex.protocol_catalog import empty_protocol_section
from backend.periscopex.af_ai_hf import append_investigated, hypotheses_from_rows
from backend.periscopex.af_trace_check import check_af_traces
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
@@ -393,6 +394,7 @@ async def run_pcb_pipeline(
summary=summary,
coverage=coverage,
not_reviewed=skipped + si_skip,
protocol_certification=empty_protocol_section().model_dump(),
)
report_path = ws.local_path("pcb_report.json")
report_path.write_text(report.model_dump_json(indent=2) + "\n")
@@ -28,6 +28,7 @@ from backend.periscopex.interface_class_check import check_interface_classes
from backend.periscopex.led_current_check import check_led_current
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport
from backend.periscopex.protocol_catalog import empty_protocol_section
from backend.periscopex.nc_pin_check import check_nc_pins
from backend.periscopex.parsers import ic_mpn_skip_reason
from backend.periscopex.passive_rail_check import (
@@ -228,6 +229,7 @@ async def validate_design_async(
coverage=_sanitize_coverage(all_coverage),
review_errors=dict(review_errors),
not_reviewed=not_reviewed,
protocol_certification=empty_protocol_section().model_dump(),
)
except Exception:
report = ValidationReport(
@@ -238,6 +240,7 @@ async def validate_design_async(
coverage={},
review_errors=dict(review_errors),
not_reviewed=not_reviewed,
protocol_certification=empty_protocol_section().model_dump(),
)
report_dict = json.loads(report.model_dump_json(indent=2))
if preserved_comments is not None:
@@ -2,6 +2,15 @@
What's new in Periscope.
## 2.64.0 — 2026-09-22 — Protocol certification M0 (catalog, no numbers)
Versioned JSON protocol pack + Python loader/validator. Catalog skeletons for USB2 / USB-C-USB2 / 100BASE-TX / HDMI (connector distinct) / DDR / LPDDR / HBM / HyperBus / QSPI-octal / eMMC / UFS / AMBA / Wishbone / Avalon / TileLink / PCIe-CXL. Constraints without an official cite are UNKNOWN / CONTROLLER_DEPENDENT / PHY_DEPENDENT / MISSING_SOURCE — **no invented 90/100/50 Ω**. `logical_protocol_id``physical_interface_id`. `pcb_relevant` is YES|NO|CONDITIONAL. RECOMMENDED is not FAIL-mandatory. Typical-as-standard is rejected by the parser. Report section **Protocolli** is empty (`nessun protocollo riconosciuto`). Recognition and L0L3 certifiers are not in this release.
- [New] `protocol_catalog.py`, `protocol_data/catalog.json`, schema 1.0.0.
- [New] `protocol_certification` on `ValidationReport`; UI section Protocolli.
- [New] pytest `tests/pcb/test_protocol_catalog_m0.py`.
- [Fixed] Native `periscope/src` wins over inherited `periscopex` on the package path.
## 2.63.5 — 2026-09-22 — AF trace analysis (trigger, cascade, visible skips)
PCB exam adds **AF trace analysis** after unchanged `run_pcb_checks` (SI/HF stay). Trigger is λ/10 or tr/(6 tpd) from FACT only. Differential pairs are one unit. Z0/Zdiff only vs a **datasheet** window. Missing tr/f/εr/stackup/Z target/via span is a visible INSUFFICIENT finding («Pista ad alta frequenza non controllata per mancanza di …»), not invented 50/90 Ω. Trigger false → no AF finding. Numerical cascade is lossless RLGC sections (ImpedenceFinder Z0); not OpenEMS/FEM. Python. PE-AF-001 AI path unchanged.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "periscope-web",
"version": "2.63.4",
"version": "2.64.0",
"private": true,
"scripts": {
"sync-version": "node scripts/sync-version.mjs",
@@ -18,6 +18,7 @@ import { useReviewedFindings } from "@/hooks/use-reviewed-findings";
import { ReportSummary } from "@/components/report/report-summary";
import { FindingsList } from "@/components/report/findings-list";
import { FindingFocusView } from "@/components/report/finding-focus-view";
import { ProtocolSection } from "@/components/report/protocol-section";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Toast, useToast } from "@/components/ui/toast";
@@ -303,6 +304,7 @@ function ReportContent({ projectId }: { projectId: string }) {
creditsSpent={creditsSpent}
totalCostUsd={totalCostUsd}
/>
<ProtocolSection section={report.protocol_certification} />
{failedReviews > 0 && (
<div className="rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 space-y-2">
<p className="text-sm font-medium text-amber-700 dark:text-amber-300">
@@ -0,0 +1,32 @@
"use client";
import type { ProtocolCertificationSection } from "@/lib/types";
export function ProtocolSection({
section,
}: {
section?: ProtocolCertificationSection | null;
}) {
const message = section?.message || "nessun protocollo riconosciuto";
const instances = section?.recognized_instances ?? [];
return (
<section className="rounded-lg border border-border bg-card p-4 space-y-2">
<h2 className="text-sm font-semibold">Protocolli</h2>
<p className="text-xs text-muted-foreground">
Logical protocol vs physical interface. M0 loads the catalog only
no recognition, no invented impedance.
</p>
{instances.length === 0 ? (
<p className="text-sm">{message}</p>
) : (
<ul className="text-sm space-y-1">
{instances.map((row, i) => (
<li key={i} className="font-mono text-xs">
{String(row.physical_interface_id ?? row.logical_protocol_id ?? i)}
</li>
))}
</ul>
)}
</section>
);
}
+9
View File
@@ -75,6 +75,15 @@ export interface ValidationReport {
comments?: Record<string, FindingComment[]>;
review_states?: Record<string, FindingReview>;
release?: ReportRelease;
protocol_certification?: ProtocolCertificationSection | null;
}
export interface ProtocolCertificationSection {
schema_version: string;
macrophase: string;
recognized_instances: Record<string, unknown>[];
message: string;
max_level_reached: string | null;
}
export type NetType = "power" | "ground" | "signal" | "unknown";
+1 -1
View File
@@ -1,3 +1,3 @@
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
export const APP_VERSION = "2.63.4";
export const APP_VERSION = "2.64.0";
export const APP_VERSION_DATE = "2026-09-22";
+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