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:
+24
-4
@@ -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__)))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 L0–L3 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 L0–L3 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,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>
|
||||
);
|
||||
}
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user