Add protocol-certification M2 L0 structural certifier (2.66.0).
Presence and connection only. FAIL solely on missing MANDATORY structure. AXI4 internal is NOT_APPLICABLE, not FAIL. RECOMMENDED never FAIL. No L1–L3 and no invented ohms/mm/ps.
This commit is contained in:
@@ -290,6 +290,10 @@ def _seed() -> None:
|
|||||||
requirement="Via series inductance from drill, span, and dielectric height.")
|
requirement="Via series inductance from drill, span, and dielectric height.")
|
||||||
_add("PE-AF-061", "RECOMMENDED", "RISK", domain="pcb",
|
_add("PE-AF-061", "RECOMMENDED", "RISK", domain="pcb",
|
||||||
requirement="Via stub length vs λ/20 when span and f are FACT.")
|
requirement="Via stub length vs λ/20 when span and f are FACT.")
|
||||||
|
_add("PE-PRT-L0-001", "MANDATORY", "RULE", domain="pcb",
|
||||||
|
requirement="Protocol nets required at L0 must exist and connect two components.")
|
||||||
|
_add("PE-PRT-L0-002", "TYPICAL", "INFO", domain="pcb",
|
||||||
|
requirement="Internal interconnect is NOT_APPLICABLE for PCB routing.")
|
||||||
|
|
||||||
|
|
||||||
_seed()
|
_seed()
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
"""M2: L0 STRUCTURAL protocol certifier. Presence and connection only.
|
||||||
|
|
||||||
|
FAIL only when a MANDATORY structural net/group is missing or unconnected.
|
||||||
|
Internal interconnects (AXI4, AMBA, HBM DQ) are NOT_APPLICABLE, never FAIL.
|
||||||
|
RECOMMENDED never FAIL. No L1–L3. No invented ohms/mm/ps.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from backend.periscopex.finding_engine import complete_finding
|
||||||
|
from backend.periscopex.models import DesignGraph, Finding
|
||||||
|
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
|
||||||
|
from backend.periscopex.protocol_catalog import (
|
||||||
|
SCHEMA_VERSION,
|
||||||
|
PhysicalInterface,
|
||||||
|
ProtocolCatalog,
|
||||||
|
ProtocolCertificationSection,
|
||||||
|
ProtocolConstraint,
|
||||||
|
empty_protocol_section,
|
||||||
|
load_catalog,
|
||||||
|
map_protocol_outcome,
|
||||||
|
)
|
||||||
|
from backend.periscopex.protocol_recognize import (
|
||||||
|
PhysicalBusInstance,
|
||||||
|
recognize_physical_buses,
|
||||||
|
)
|
||||||
|
|
||||||
|
PACK_MACROPHASE = "M2"
|
||||||
|
SOURCE = "protocol_l0"
|
||||||
|
LEVEL = "L0"
|
||||||
|
|
||||||
|
L0_CHECKS = frozenset({
|
||||||
|
"topology",
|
||||||
|
"byte_lane_mapping",
|
||||||
|
"cmd_dat_grouping",
|
||||||
|
"clk_reference",
|
||||||
|
"cc_rd_rp",
|
||||||
|
"vbus_gnd",
|
||||||
|
"pcb_routing",
|
||||||
|
"pcb_individual_dq_routing",
|
||||||
|
"dq_group",
|
||||||
|
"superspeed_pairs",
|
||||||
|
})
|
||||||
|
|
||||||
|
_CC_RE = re.compile(r"(?:^|[_/.])CC[12]$", re.I)
|
||||||
|
_VBUS_RE = re.compile(r"VBUS|\+?VUSB|USB_VBUS", re.I)
|
||||||
|
_GND_RE = re.compile(r"(?:^|[_/.])(?:GND|VSS|DGND)(?:$|[_/.])", re.I)
|
||||||
|
_SS_RE = re.compile(r"SSTX|SSRX|SS_T[XR]|USB3|USB_SS", re.I)
|
||||||
|
|
||||||
|
MandatoryClass = Literal["MANDATORY", "RECOMMENDED", "OPTIONAL", "INFORMATIONAL"]
|
||||||
|
|
||||||
|
|
||||||
|
class L0CheckResult(BaseModel):
|
||||||
|
check: str
|
||||||
|
level: Literal["L0"] = "L0"
|
||||||
|
result: Literal[
|
||||||
|
"PASS", "FAIL", "WARNING", "UNKNOWN", "NOT_APPLICABLE", "VENDOR_DEPENDENT",
|
||||||
|
]
|
||||||
|
mandatory: MandatoryClass
|
||||||
|
nets: list[str] = Field(default_factory=list)
|
||||||
|
notes: str = ""
|
||||||
|
finding_class: str = ""
|
||||||
|
status: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _leaf(name: str) -> str:
|
||||||
|
n = normalize_kicad_hierarchy_net(name)
|
||||||
|
return n.split("/")[-1] if n else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _net_present(graph: DesignGraph, name: str) -> bool:
|
||||||
|
return name in graph.nets
|
||||||
|
|
||||||
|
|
||||||
|
def _net_connected(graph: DesignGraph, name: str) -> bool:
|
||||||
|
net = graph.nets.get(name)
|
||||||
|
if not net:
|
||||||
|
return False
|
||||||
|
refs = {p.component_ref for p in net.pins if p.component_ref}
|
||||||
|
return len(refs) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def _constraint_for(iface: PhysicalInterface, check: str) -> ProtocolConstraint | None:
|
||||||
|
aliases = {
|
||||||
|
"cc_rd_rp": "cc_termination",
|
||||||
|
"pcb_routing": "pcb_routing",
|
||||||
|
"pcb_individual_dq_routing": "pcb_individual_dq_routing",
|
||||||
|
"superspeed_pairs": "superspeed_pairs",
|
||||||
|
"topology": "topology",
|
||||||
|
"byte_lane_mapping": "topology",
|
||||||
|
}
|
||||||
|
want = aliases.get(check, check)
|
||||||
|
for c in iface.constraints:
|
||||||
|
if c.parameter == want or c.parameter == check:
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _mandatory(iface: PhysicalInterface, check: str) -> MandatoryClass:
|
||||||
|
c = _constraint_for(iface, check)
|
||||||
|
if c:
|
||||||
|
return c.mandatory
|
||||||
|
if check in {"superspeed_pairs", "pcb_routing", "pcb_individual_dq_routing"}:
|
||||||
|
return "INFORMATIONAL"
|
||||||
|
return "MANDATORY"
|
||||||
|
|
||||||
|
|
||||||
|
def _pack(check: str, result: str, mandatory: MandatoryClass, *,
|
||||||
|
nets: list[str] | None = None, notes: str = "") -> L0CheckResult:
|
||||||
|
if result == "FAIL" and mandatory != "MANDATORY":
|
||||||
|
result = "WARNING"
|
||||||
|
cls, status, _ev = map_protocol_outcome(result, mandatory) # type: ignore[arg-type]
|
||||||
|
return L0CheckResult(
|
||||||
|
check=check, result=result, mandatory=mandatory, # type: ignore[arg-type]
|
||||||
|
nets=nets or [], notes=notes,
|
||||||
|
finding_class=cls, status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _structural_nets_ok(graph: DesignGraph, names: list[str]) -> tuple[bool, str]:
|
||||||
|
missing = [n for n in names if not _net_present(graph, n)]
|
||||||
|
if missing:
|
||||||
|
return False, f"missing net(s): {', '.join(missing)}"
|
||||||
|
dangling = [n for n in names if not _net_connected(graph, n)]
|
||||||
|
if dangling:
|
||||||
|
return False, f"unconnected net(s): {', '.join(dangling)}"
|
||||||
|
return True, "present and connected"
|
||||||
|
|
||||||
|
|
||||||
|
def certify_instance_l0(
|
||||||
|
graph: DesignGraph,
|
||||||
|
inst: PhysicalBusInstance,
|
||||||
|
iface: PhysicalInterface,
|
||||||
|
) -> list[L0CheckResult]:
|
||||||
|
if inst.pcb_relevant == "NO" or iface.pcb_relevant == "NO":
|
||||||
|
return [_pack(
|
||||||
|
"pcb_routing", "NOT_APPLICABLE", "INFORMATIONAL",
|
||||||
|
notes="Internal interconnect — not PCB routing. Not FAIL.",
|
||||||
|
)]
|
||||||
|
out: list[L0CheckResult] = []
|
||||||
|
checks = [c for c in iface.required_checks if c in L0_CHECKS]
|
||||||
|
if not checks:
|
||||||
|
checks = ["topology"]
|
||||||
|
for check in checks:
|
||||||
|
mand = _mandatory(iface, check)
|
||||||
|
out.append(_run_l0_check(graph, inst, check, mand))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _run_l0_check(
|
||||||
|
graph: DesignGraph,
|
||||||
|
inst: PhysicalBusInstance,
|
||||||
|
check: str,
|
||||||
|
mand: MandatoryClass,
|
||||||
|
) -> L0CheckResult:
|
||||||
|
if check in {"pcb_routing", "pcb_individual_dq_routing"}:
|
||||||
|
return _pack(check, "NOT_APPLICABLE", mand,
|
||||||
|
notes="Not a PCB-routed individual channel at L0.")
|
||||||
|
if check == "superspeed_pairs":
|
||||||
|
ss = [n for n in graph.nets if _SS_RE.search(_leaf(n))]
|
||||||
|
return _pack(
|
||||||
|
check, "NOT_APPLICABLE", mand, nets=ss,
|
||||||
|
notes="USB2-only physical interface: SuperSpeed pairs not required.",
|
||||||
|
)
|
||||||
|
if check == "topology":
|
||||||
|
names: list[str] = []
|
||||||
|
for g in inst.groups:
|
||||||
|
if g.kind == "DIFFERENTIAL_PAIR":
|
||||||
|
names.extend(g.nets)
|
||||||
|
if not names:
|
||||||
|
names = list(inst.nets)
|
||||||
|
if len(names) < 2:
|
||||||
|
return _pack(
|
||||||
|
check, "FAIL", mand, nets=names,
|
||||||
|
notes="MANDATORY differential pair or bus nets not present.",
|
||||||
|
)
|
||||||
|
ok, msg = _structural_nets_ok(graph, names)
|
||||||
|
return _pack(check, "PASS" if ok else "FAIL", mand, nets=names, notes=msg)
|
||||||
|
if check == "byte_lane_mapping" or check == "dq_group":
|
||||||
|
lanes = [g for g in inst.groups if g.kind == "BYTE_LANE"]
|
||||||
|
if not lanes:
|
||||||
|
return _pack(
|
||||||
|
check, "FAIL", mand,
|
||||||
|
notes="MANDATORY byte-lane / DQ group not reconstructed.",
|
||||||
|
)
|
||||||
|
names = [n for g in lanes for n in g.nets]
|
||||||
|
ok, msg = _structural_nets_ok(graph, names)
|
||||||
|
return _pack(check, "PASS" if ok else "FAIL", mand, nets=names, notes=msg)
|
||||||
|
if check == "cmd_dat_grouping":
|
||||||
|
names = [n for g in inst.groups for n in g.nets]
|
||||||
|
if not names:
|
||||||
|
return _pack(check, "FAIL", mand, notes="No CMD/DAT group nets.")
|
||||||
|
ok, msg = _structural_nets_ok(graph, names)
|
||||||
|
return _pack(check, "PASS" if ok else "FAIL", mand, nets=names, notes=msg)
|
||||||
|
if check == "clk_reference":
|
||||||
|
clocks = [n for g in inst.groups if g.kind == "CLOCK_GROUP" for n in g.nets]
|
||||||
|
if not clocks:
|
||||||
|
return _pack(check, "FAIL", mand, notes="Clock reference net missing.")
|
||||||
|
ok, msg = _structural_nets_ok(graph, clocks)
|
||||||
|
return _pack(check, "PASS" if ok else "FAIL", mand, nets=clocks, notes=msg)
|
||||||
|
if check == "cc_rd_rp":
|
||||||
|
ccs = [n for n in graph.nets if _CC_RE.search(_leaf(n))]
|
||||||
|
if len(ccs) < 1:
|
||||||
|
return _pack(check, "FAIL", mand, notes="CC1/CC2 net not present.")
|
||||||
|
ok, msg = _structural_nets_ok(graph, ccs)
|
||||||
|
return _pack(check, "PASS" if ok else "FAIL", mand, nets=ccs, notes=msg)
|
||||||
|
if check == "vbus_gnd":
|
||||||
|
vbus = [n for n in graph.nets if _VBUS_RE.search(_leaf(n))]
|
||||||
|
gnd = [n for n in graph.nets if _GND_RE.search(_leaf(n))]
|
||||||
|
names = vbus + gnd
|
||||||
|
if not vbus or not gnd:
|
||||||
|
return _pack(
|
||||||
|
check, "FAIL", mand, nets=names,
|
||||||
|
notes="VBUS and/or GND net not present.",
|
||||||
|
)
|
||||||
|
ok, msg = _structural_nets_ok(graph, names)
|
||||||
|
return _pack(check, "PASS" if ok else "FAIL", mand, nets=names, notes=msg)
|
||||||
|
return _pack(check, "NOT_APPLICABLE", mand, notes="Not an L0 structural check.")
|
||||||
|
|
||||||
|
|
||||||
|
def l0_findings(
|
||||||
|
inst: PhysicalBusInstance,
|
||||||
|
results: list[L0CheckResult],
|
||||||
|
) -> list[Finding]:
|
||||||
|
out: list[Finding] = []
|
||||||
|
designator = inst.host_ref or inst.physical_interface_id
|
||||||
|
for r in results:
|
||||||
|
if r.result == "PASS":
|
||||||
|
continue
|
||||||
|
if r.result == "FAIL" and r.mandatory != "MANDATORY":
|
||||||
|
continue
|
||||||
|
status = r.status if r.status in {"ERROR", "WARNING", "INFO"} else "INFO"
|
||||||
|
if r.result == "FAIL":
|
||||||
|
rule_id = "PE-PRT-L0-001"
|
||||||
|
finding = (
|
||||||
|
f"L0 structural: {r.check} {r.notes or 'missing MANDATORY net/connection'}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rule_id = "PE-PRT-L0-002"
|
||||||
|
finding = f"L0 {r.result}: {r.check} — {r.notes}".strip()
|
||||||
|
f = Finding(
|
||||||
|
designator=designator,
|
||||||
|
mpn="",
|
||||||
|
aspect="protocol_l0",
|
||||||
|
finding=finding,
|
||||||
|
why=r.notes,
|
||||||
|
status=status,
|
||||||
|
source=SOURCE,
|
||||||
|
net=r.nets[0] if r.nets else None,
|
||||||
|
rule_id=rule_id,
|
||||||
|
facts=finding,
|
||||||
|
requirement=(
|
||||||
|
"MANDATORY protocol nets must exist and connect at least two components."
|
||||||
|
if r.result == "FAIL"
|
||||||
|
else "Internal protocol is not certified as PCB routing."
|
||||||
|
),
|
||||||
|
inference=f"{LEVEL} {r.result} (not electrical; no invented Z).",
|
||||||
|
provenance="MANDATORY" if r.mandatory == "MANDATORY" else "TYPICAL",
|
||||||
|
finding_class=r.finding_class if r.finding_class in {"RULE", "RISK", "REVIEW", "INFO"} else "INFO",
|
||||||
|
)
|
||||||
|
out.append(complete_finding(f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def certify_l0(
|
||||||
|
graph: DesignGraph,
|
||||||
|
instances: list[PhysicalBusInstance],
|
||||||
|
catalog: ProtocolCatalog | None = None,
|
||||||
|
) -> tuple[dict[str, list[L0CheckResult]], list[Finding]]:
|
||||||
|
cat = catalog or load_catalog()
|
||||||
|
ifaces = {p.id: p for p in cat.physical_interfaces}
|
||||||
|
by_id: dict[str, list[L0CheckResult]] = {}
|
||||||
|
findings: list[Finding] = []
|
||||||
|
for inst in instances:
|
||||||
|
iface = ifaces.get(inst.physical_interface_id)
|
||||||
|
if iface is None:
|
||||||
|
continue
|
||||||
|
rows = certify_instance_l0(graph, inst, iface)
|
||||||
|
by_id[inst.instance_id] = rows
|
||||||
|
findings.extend(l0_findings(inst, rows))
|
||||||
|
return by_id, findings
|
||||||
|
|
||||||
|
|
||||||
|
def protocol_exam(
|
||||||
|
graph: DesignGraph | None,
|
||||||
|
catalog: ProtocolCatalog | None = None,
|
||||||
|
) -> tuple[ProtocolCertificationSection, list[Finding]]:
|
||||||
|
if graph is None:
|
||||||
|
return empty_protocol_section(), []
|
||||||
|
cat = catalog or load_catalog()
|
||||||
|
instances = recognize_physical_buses(graph, cat)
|
||||||
|
if not instances:
|
||||||
|
sec = empty_protocol_section()
|
||||||
|
sec.macrophase = PACK_MACROPHASE
|
||||||
|
return sec, []
|
||||||
|
l0, findings = certify_l0(graph, instances, cat)
|
||||||
|
dumps: list[dict[str, Any]] = []
|
||||||
|
for inst in instances:
|
||||||
|
row = inst.model_dump()
|
||||||
|
row["l0_checks"] = [c.model_dump() for c in l0.get(inst.instance_id, [])]
|
||||||
|
dumps.append(row)
|
||||||
|
sec = ProtocolCertificationSection(
|
||||||
|
schema_version=SCHEMA_VERSION,
|
||||||
|
macrophase=PACK_MACROPHASE,
|
||||||
|
recognized_instances=dumps,
|
||||||
|
message="",
|
||||||
|
max_level_reached=LEVEL,
|
||||||
|
)
|
||||||
|
return sec, findings
|
||||||
@@ -14,11 +14,8 @@ from pydantic import BaseModel, Field
|
|||||||
from backend.periscopex.models import Component, DesignGraph
|
from backend.periscopex.models import Component, DesignGraph
|
||||||
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
|
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
|
||||||
from backend.periscopex.protocol_catalog import (
|
from backend.periscopex.protocol_catalog import (
|
||||||
EMPTY_PROTOCOL_MESSAGE,
|
|
||||||
SCHEMA_VERSION,
|
|
||||||
ProtocolCatalog,
|
ProtocolCatalog,
|
||||||
ProtocolCertificationSection,
|
ProtocolCertificationSection,
|
||||||
empty_protocol_section,
|
|
||||||
load_catalog,
|
load_catalog,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -548,17 +545,6 @@ def protocol_section_for_graph(
|
|||||||
graph: DesignGraph | None,
|
graph: DesignGraph | None,
|
||||||
catalog: ProtocolCatalog | None = None,
|
catalog: ProtocolCatalog | None = None,
|
||||||
) -> ProtocolCertificationSection:
|
) -> ProtocolCertificationSection:
|
||||||
if graph is None:
|
from backend.periscopex.protocol_l0 import protocol_exam
|
||||||
return empty_protocol_section()
|
|
||||||
instances = recognize_physical_buses(graph, catalog)
|
return protocol_exam(graph, catalog)[0]
|
||||||
if not instances:
|
|
||||||
sec = empty_protocol_section()
|
|
||||||
sec.macrophase = PACK_MACROPHASE
|
|
||||||
return sec
|
|
||||||
return ProtocolCertificationSection(
|
|
||||||
schema_version=SCHEMA_VERSION,
|
|
||||||
macrophase=PACK_MACROPHASE,
|
|
||||||
recognized_instances=[i.model_dump() for i in instances],
|
|
||||||
message="",
|
|
||||||
max_level_reached=None,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from backend.periscopex.layout_rules import needs_layout_rules_refresh
|
|||||||
from backend.periscopex.models import (
|
from backend.periscopex.models import (
|
||||||
ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, ValidationReport,
|
ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, ValidationReport,
|
||||||
)
|
)
|
||||||
from backend.periscopex.protocol_recognize import protocol_section_for_graph
|
from backend.periscopex.protocol_l0 import protocol_exam
|
||||||
from backend.periscopex.af_ai_hf import append_investigated, hypotheses_from_rows
|
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.af_trace_check import check_af_traces
|
||||||
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
|
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
|
||||||
@@ -376,6 +376,8 @@ async def run_pcb_pipeline(
|
|||||||
return
|
return
|
||||||
|
|
||||||
_step(project_id, "write_report", "running")
|
_step(project_id, "write_report", "running")
|
||||||
|
proto_section, proto_findings = protocol_exam(graph)
|
||||||
|
findings.extend(proto_findings)
|
||||||
assign_pcb_finding_ids(findings)
|
assign_pcb_finding_ids(findings)
|
||||||
annotate_findings_cad(findings, cad_index_from_graph(graph))
|
annotate_findings_cad(findings, cad_index_from_graph(graph))
|
||||||
dec_path = ws.local_path("decisions.json")
|
dec_path = ws.local_path("decisions.json")
|
||||||
@@ -394,7 +396,7 @@ async def run_pcb_pipeline(
|
|||||||
summary=summary,
|
summary=summary,
|
||||||
coverage=coverage,
|
coverage=coverage,
|
||||||
not_reviewed=skipped + si_skip,
|
not_reviewed=skipped + si_skip,
|
||||||
protocol_certification=protocol_section_for_graph(graph).model_dump(),
|
protocol_certification=proto_section.model_dump(),
|
||||||
)
|
)
|
||||||
report_path = ws.local_path("pcb_report.json")
|
report_path = ws.local_path("pcb_report.json")
|
||||||
report_path.write_text(report.model_dump_json(indent=2) + "\n")
|
report_path.write_text(report.model_dump_json(indent=2) + "\n")
|
||||||
|
|||||||
@@ -28,7 +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.led_current_check import check_led_current
|
||||||
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
|
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
|
||||||
from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport
|
from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport
|
||||||
from backend.periscopex.protocol_recognize import protocol_section_for_graph
|
from backend.periscopex.protocol_l0 import protocol_exam
|
||||||
from backend.periscopex.nc_pin_check import check_nc_pins
|
from backend.periscopex.nc_pin_check import check_nc_pins
|
||||||
from backend.periscopex.parsers import ic_mpn_skip_reason
|
from backend.periscopex.parsers import ic_mpn_skip_reason
|
||||||
from backend.periscopex.passive_rail_check import (
|
from backend.periscopex.passive_rail_check import (
|
||||||
@@ -209,6 +209,8 @@ async def validate_design_async(
|
|||||||
return clean
|
return clean
|
||||||
|
|
||||||
def _write_report(paused: bool = False) -> ValidationReport:
|
def _write_report(paused: bool = False) -> ValidationReport:
|
||||||
|
proto_section, proto_findings = protocol_exam(graph)
|
||||||
|
all_findings.extend(proto_findings)
|
||||||
annotate_findings_cad(all_findings, graph.cad_index)
|
annotate_findings_cad(all_findings, graph.cad_index)
|
||||||
assign_finding_ids(all_findings)
|
assign_finding_ids(all_findings)
|
||||||
dec_path = existing_path.with_name("decisions.json")
|
dec_path = existing_path.with_name("decisions.json")
|
||||||
@@ -229,7 +231,7 @@ async def validate_design_async(
|
|||||||
coverage=_sanitize_coverage(all_coverage),
|
coverage=_sanitize_coverage(all_coverage),
|
||||||
review_errors=dict(review_errors),
|
review_errors=dict(review_errors),
|
||||||
not_reviewed=not_reviewed,
|
not_reviewed=not_reviewed,
|
||||||
protocol_certification=protocol_section_for_graph(graph).model_dump(),
|
protocol_certification=proto_section.model_dump(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
report = ValidationReport(
|
report = ValidationReport(
|
||||||
@@ -240,7 +242,7 @@ async def validate_design_async(
|
|||||||
coverage={},
|
coverage={},
|
||||||
review_errors=dict(review_errors),
|
review_errors=dict(review_errors),
|
||||||
not_reviewed=not_reviewed,
|
not_reviewed=not_reviewed,
|
||||||
protocol_certification=protocol_section_for_graph(graph).model_dump(),
|
protocol_certification=proto_section.model_dump(),
|
||||||
)
|
)
|
||||||
report_dict = json.loads(report.model_dump_json(indent=2))
|
report_dict = json.loads(report.model_dump_json(indent=2))
|
||||||
if preserved_comments is not None:
|
if preserved_comments is not None:
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 2.66.0 — 2026-09-22 — Protocol certification M2 (L0 structural)
|
||||||
|
|
||||||
|
L0 STRUCTURAL certifier: nets must exist and connect at least two components. FAIL only for missing **MANDATORY** structure. AXI4 / internal interconnects are **NOT_APPLICABLE** (not FAIL). RECOMMENDED never FAIL. No L1–L3, no invented ohms/mm/ps.
|
||||||
|
|
||||||
|
- [New] `protocol_l0.py`; `PE-PRT-L0-001` / `PE-PRT-L0-002`.
|
||||||
|
- [New] pytest `tests/pcb/test_protocol_l0_m2.py`.
|
||||||
|
|
||||||
## 2.65.0 — 2026-09-22 — Protocol certification M1 (instance recognition)
|
## 2.65.0 — 2026-09-22 — Protocol certification M1 (instance recognition)
|
||||||
|
|
||||||
`physical_bus_instance` from declaration / silicon / part / pin / net / pairs. Output: instances, signal groups (DIFF pair, byte lane, clock/address), confidence. Ambiguity is **REVIEW**, never FAIL. AXI4 internal is recognised with `pcb_relevant: NO` and **no PCB nets**. No L0–L3 certifier. No invented ohms.
|
`physical_bus_instance` from declaration / silicon / part / pin / net / pairs. Output: instances, signal groups (DIFF pair, byte lane, clock/address), confidence. Ambiguity is **REVIEW**, never FAIL. AXI4 internal is recognised with `pcb_relevant: NO` and **no PCB nets**. No L0–L3 certifier. No invented ohms.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "periscope-web",
|
"name": "periscope-web",
|
||||||
"version": "2.65.0",
|
"version": "2.66.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"sync-version": "node scripts/sync-version.mjs",
|
"sync-version": "node scripts/sync-version.mjs",
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export function ProtocolSection({
|
|||||||
<section className="rounded-lg border border-border bg-card p-4 space-y-2">
|
<section className="rounded-lg border border-border bg-card p-4 space-y-2">
|
||||||
<h2 className="text-sm font-semibold">Protocolli</h2>
|
<h2 className="text-sm font-semibold">Protocolli</h2>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Logical protocol vs physical interface. Recognition only (M1) —
|
Logical protocol vs physical interface. L0 structural only —
|
||||||
no L0–L3 certifier, no invented impedance.
|
presence and connection, no invented impedance.
|
||||||
</p>
|
</p>
|
||||||
{instances.length === 0 ? (
|
{instances.length === 0 ? (
|
||||||
<p className="text-sm">{message}</p>
|
<p className="text-sm">{message}</p>
|
||||||
@@ -43,6 +43,14 @@ export function ProtocolSection({
|
|||||||
.join(", ")}
|
.join(", ")}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
{Array.isArray(row.l0_checks) && row.l0_checks.length > 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
L0{" "}
|
||||||
|
{(row.l0_checks as Record<string, unknown>[])
|
||||||
|
.map((c) => `${String(c.check)}=${String(c.result)}`)
|
||||||
|
.join("; ")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function isLayoutFinding(f: {
|
|||||||
f.source === "pdn_check" ||
|
f.source === "pdn_check" ||
|
||||||
f.source === "af_ai_hf" ||
|
f.source === "af_ai_hf" ||
|
||||||
f.source === "af_trace_check" ||
|
f.source === "af_trace_check" ||
|
||||||
|
f.source === "protocol_l0" ||
|
||||||
rid.startsWith("PE-PLC") ||
|
rid.startsWith("PE-PLC") ||
|
||||||
rid.startsWith("PE-LAY") ||
|
rid.startsWith("PE-LAY") ||
|
||||||
rid.startsWith("PE-SI") ||
|
rid.startsWith("PE-SI") ||
|
||||||
@@ -50,6 +51,7 @@ export function isLayoutFinding(f: {
|
|||||||
rid.startsWith("PE-ANT") ||
|
rid.startsWith("PE-ANT") ||
|
||||||
rid.startsWith("PE-PDN") ||
|
rid.startsWith("PE-PDN") ||
|
||||||
rid.startsWith("PE-AF") ||
|
rid.startsWith("PE-AF") ||
|
||||||
|
rid.startsWith("PE-PRT") ||
|
||||||
/^PE-BOM-01[0-4]$/.test(rid)
|
/^PE-BOM-01[0-4]$/.test(rid)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
|
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
|
||||||
export const APP_VERSION = "2.65.0";
|
export const APP_VERSION = "2.66.0";
|
||||||
export const APP_VERSION_DATE = "2026-09-22";
|
export const APP_VERSION_DATE = "2026-09-22";
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"""M2 L0 structural certifier: presence/connection; AXI N/A; RECOMMENDED not FAIL."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from backend.periscopex.models import (
|
||||||
|
Component,
|
||||||
|
ComponentType,
|
||||||
|
DesignGraph,
|
||||||
|
Net,
|
||||||
|
NetType,
|
||||||
|
PinConnection,
|
||||||
|
)
|
||||||
|
from backend.periscopex.protocol_l0 import (
|
||||||
|
PACK_MACROPHASE,
|
||||||
|
_pack,
|
||||||
|
certify_l0,
|
||||||
|
protocol_exam,
|
||||||
|
)
|
||||||
|
from backend.periscopex.protocol_recognize import recognize_physical_buses
|
||||||
|
|
||||||
|
|
||||||
|
def _ic(ref: str, pins: dict[str, str], *, mpn: str = "", value: str = "") -> Component:
|
||||||
|
return Component(
|
||||||
|
reference=ref, value=value or mpn, footprint="",
|
||||||
|
component_type=ComponentType.IC, mpn=mpn or None, pins=pins,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _net(name: str, *pairs: tuple[str, str]) -> Net:
|
||||||
|
return Net(
|
||||||
|
name=name, net_type=NetType.SIGNAL,
|
||||||
|
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _usb_connected() -> DesignGraph:
|
||||||
|
return DesignGraph(
|
||||||
|
components={
|
||||||
|
"U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}, mpn="CH340E"),
|
||||||
|
"J2": Component(
|
||||||
|
reference="J2", value="USB2_TypeA",
|
||||||
|
footprint="USB_A",
|
||||||
|
component_type=ComponentType.CONNECTOR,
|
||||||
|
pins={"2": "USB_D+", "3": "USB_D-"},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
nets={
|
||||||
|
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "2")),
|
||||||
|
"USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "3")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _usb_dangling_minus() -> DesignGraph:
|
||||||
|
return DesignGraph(
|
||||||
|
components={
|
||||||
|
"U1": _ic("U1", {"1": "USB_D+"}, mpn="CH340E"),
|
||||||
|
"J2": Component(
|
||||||
|
reference="J2", value="USB2_TypeA",
|
||||||
|
footprint="USB_A",
|
||||||
|
component_type=ComponentType.CONNECTOR,
|
||||||
|
pins={"2": "USB_D+", "3": "USB_D-"},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
nets={
|
||||||
|
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "2")),
|
||||||
|
"USB_D-": _net("USB_D-", ("J2", "3")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _axi_graph() -> DesignGraph:
|
||||||
|
return DesignGraph(
|
||||||
|
components={
|
||||||
|
"U3": _ic("U3", {"1": "AXI_AWVALID"}, mpn="XC7A100T", value="AXI4 interconnect"),
|
||||||
|
},
|
||||||
|
nets={"AXI_AWVALID": _net("AXI_AWVALID", ("U3", "1"))},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ddr_one_ended() -> DesignGraph:
|
||||||
|
pins = {str(i): f"DDR4_DQ{i}" for i in range(8)}
|
||||||
|
pins.update({"8": "DDR4_DQS0_P", "9": "DDR4_DQS0_N"})
|
||||||
|
return DesignGraph(
|
||||||
|
components={"U5": _ic("U5", pins, mpn="MT41K256M16TW", value="DDR4")},
|
||||||
|
nets={n: _net(n, ("U5", p)) for p, n in pins.items()},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _l0_for(graph: DesignGraph, logical_sub: str) -> list:
|
||||||
|
insts = recognize_physical_buses(graph)
|
||||||
|
by, _ = certify_l0(graph, insts)
|
||||||
|
rows = []
|
||||||
|
for inst in insts:
|
||||||
|
if logical_sub in inst.logical_protocol_id:
|
||||||
|
rows.extend(by.get(inst.instance_id, []))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def test_usb_connected_pair_l0_topology_pass():
|
||||||
|
rows = _l0_for(_usb_connected(), "usb2")
|
||||||
|
topo = [r for r in rows if r.check == "topology"]
|
||||||
|
assert topo
|
||||||
|
assert topo[0].result == "PASS"
|
||||||
|
assert topo[0].level == "L0"
|
||||||
|
blob = json.dumps(topo[0].model_dump())
|
||||||
|
assert "ohm" not in blob.lower()
|
||||||
|
assert "90" not in blob
|
||||||
|
|
||||||
|
|
||||||
|
def test_usb_unconnected_dminus_l0_fail_mandatory():
|
||||||
|
g = _usb_dangling_minus()
|
||||||
|
insts = recognize_physical_buses(g)
|
||||||
|
usb = [i for i in insts if "usb" in i.logical_protocol_id]
|
||||||
|
assert usb
|
||||||
|
by, findings = certify_l0(g, insts)
|
||||||
|
rows = [r for i in usb for r in by[i.instance_id]]
|
||||||
|
topo = [r for r in rows if r.check == "topology"]
|
||||||
|
assert topo
|
||||||
|
assert topo[0].result == "FAIL"
|
||||||
|
assert topo[0].mandatory == "MANDATORY"
|
||||||
|
assert any(f.status == "ERROR" and f.rule_id == "PE-PRT-L0-001" for f in findings)
|
||||||
|
|
||||||
|
|
||||||
|
def test_axi_internal_l0_not_applicable_not_fail():
|
||||||
|
rows = _l0_for(_axi_graph(), "axi4")
|
||||||
|
assert rows
|
||||||
|
assert all(r.result == "NOT_APPLICABLE" for r in rows)
|
||||||
|
assert all(r.result != "FAIL" for r in rows)
|
||||||
|
sec, findings = protocol_exam(_axi_graph())
|
||||||
|
assert sec.max_level_reached == "L0"
|
||||||
|
assert sec.macrophase == PACK_MACROPHASE
|
||||||
|
assert all(f.status != "ERROR" for f in findings)
|
||||||
|
assert all("AXI_AWVALID" not in (f.net or "") for f in findings if f.rule_id == "PE-PRT-L0-001")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ddr_unconnected_memory_fails_l0_connection():
|
||||||
|
rows = _l0_for(_ddr_one_ended(), "ddr4")
|
||||||
|
mapping = [r for r in rows if r.check == "byte_lane_mapping"]
|
||||||
|
assert mapping
|
||||||
|
assert mapping[0].result == "FAIL"
|
||||||
|
assert "unconnected" in mapping[0].notes
|
||||||
|
|
||||||
|
|
||||||
|
def test_recommended_never_parses_as_fail():
|
||||||
|
rec = _pack("return_path", "FAIL", "RECOMMENDED", notes="missing")
|
||||||
|
assert rec.result != "FAIL"
|
||||||
|
assert rec.status != "ERROR"
|
||||||
|
|
||||||
|
|
||||||
|
def test_l0_does_not_run_impedance():
|
||||||
|
rows = _l0_for(_usb_connected(), "usb2")
|
||||||
|
assert not any(r.check in {"differential_impedance", "intra_pair_skew", "length"} for r in rows)
|
||||||
@@ -13,7 +13,6 @@ from backend.periscopex.models import (
|
|||||||
PinConnection,
|
PinConnection,
|
||||||
)
|
)
|
||||||
from backend.periscopex.protocol_recognize import (
|
from backend.periscopex.protocol_recognize import (
|
||||||
PACK_MACROPHASE,
|
|
||||||
protocol_section_for_graph,
|
protocol_section_for_graph,
|
||||||
recognize_physical_buses,
|
recognize_physical_buses,
|
||||||
)
|
)
|
||||||
@@ -143,7 +142,6 @@ def test_empty_graph_keeps_nessun_protocollo():
|
|||||||
sec = protocol_section_for_graph(DesignGraph())
|
sec = protocol_section_for_graph(DesignGraph())
|
||||||
assert sec.message == "nessun protocollo riconosciuto"
|
assert sec.message == "nessun protocollo riconosciuto"
|
||||||
assert sec.recognized_instances == []
|
assert sec.recognized_instances == []
|
||||||
assert sec.macrophase == PACK_MACROPHASE
|
|
||||||
blob = json.dumps(sec.model_dump())
|
blob = json.dumps(sec.model_dump())
|
||||||
assert "ohm" not in blob.lower()
|
assert "ohm" not in blob.lower()
|
||||||
|
|
||||||
@@ -151,7 +149,6 @@ def test_empty_graph_keeps_nessun_protocollo():
|
|||||||
def test_report_section_lists_usb_without_z():
|
def test_report_section_lists_usb_without_z():
|
||||||
sec = protocol_section_for_graph(_usb_graph())
|
sec = protocol_section_for_graph(_usb_graph())
|
||||||
assert sec.recognized_instances
|
assert sec.recognized_instances
|
||||||
assert sec.max_level_reached is None
|
|
||||||
blob = json.dumps(sec.model_dump())
|
blob = json.dumps(sec.model_dump())
|
||||||
assert "90" not in blob
|
assert "90" not in blob
|
||||||
assert "Zdiff" not in blob
|
assert "Zdiff" not in blob
|
||||||
|
|||||||
Reference in New Issue
Block a user