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.")
|
||||
_add("PE-AF-061", "RECOMMENDED", "RISK", domain="pcb",
|
||||
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()
|
||||
|
||||
@@ -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.pcb_net_match import normalize_kicad_hierarchy_net
|
||||
from backend.periscopex.protocol_catalog import (
|
||||
EMPTY_PROTOCOL_MESSAGE,
|
||||
SCHEMA_VERSION,
|
||||
ProtocolCatalog,
|
||||
ProtocolCertificationSection,
|
||||
empty_protocol_section,
|
||||
load_catalog,
|
||||
)
|
||||
|
||||
@@ -548,17 +545,6 @@ def protocol_section_for_graph(
|
||||
graph: DesignGraph | None,
|
||||
catalog: ProtocolCatalog | None = None,
|
||||
) -> ProtocolCertificationSection:
|
||||
if graph is None:
|
||||
return empty_protocol_section()
|
||||
instances = recognize_physical_buses(graph, catalog)
|
||||
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,
|
||||
)
|
||||
from backend.periscopex.protocol_l0 import protocol_exam
|
||||
|
||||
return protocol_exam(graph, catalog)[0]
|
||||
|
||||
@@ -28,7 +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_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_trace_check import check_af_traces
|
||||
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
|
||||
@@ -376,6 +376,8 @@ async def run_pcb_pipeline(
|
||||
return
|
||||
|
||||
_step(project_id, "write_report", "running")
|
||||
proto_section, proto_findings = protocol_exam(graph)
|
||||
findings.extend(proto_findings)
|
||||
assign_pcb_finding_ids(findings)
|
||||
annotate_findings_cad(findings, cad_index_from_graph(graph))
|
||||
dec_path = ws.local_path("decisions.json")
|
||||
@@ -394,7 +396,7 @@ async def run_pcb_pipeline(
|
||||
summary=summary,
|
||||
coverage=coverage,
|
||||
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.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.lifecycle import check_lifecycle, load_lifecycle_dir
|
||||
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.parsers import ic_mpn_skip_reason
|
||||
from backend.periscopex.passive_rail_check import (
|
||||
@@ -209,6 +209,8 @@ async def validate_design_async(
|
||||
return clean
|
||||
|
||||
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)
|
||||
assign_finding_ids(all_findings)
|
||||
dec_path = existing_path.with_name("decisions.json")
|
||||
@@ -229,7 +231,7 @@ async def validate_design_async(
|
||||
coverage=_sanitize_coverage(all_coverage),
|
||||
review_errors=dict(review_errors),
|
||||
not_reviewed=not_reviewed,
|
||||
protocol_certification=protocol_section_for_graph(graph).model_dump(),
|
||||
protocol_certification=proto_section.model_dump(),
|
||||
)
|
||||
except Exception:
|
||||
report = ValidationReport(
|
||||
@@ -240,7 +242,7 @@ async def validate_design_async(
|
||||
coverage={},
|
||||
review_errors=dict(review_errors),
|
||||
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))
|
||||
if preserved_comments is not None:
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
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)
|
||||
|
||||
`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",
|
||||
"version": "2.65.0",
|
||||
"version": "2.66.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"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">
|
||||
<h2 className="text-sm font-semibold">Protocolli</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Logical protocol vs physical interface. Recognition only (M1) —
|
||||
no L0–L3 certifier, no invented impedance.
|
||||
Logical protocol vs physical interface. L0 structural only —
|
||||
presence and connection, no invented impedance.
|
||||
</p>
|
||||
{instances.length === 0 ? (
|
||||
<p className="text-sm">{message}</p>
|
||||
@@ -43,6 +43,14 @@ export function ProtocolSection({
|
||||
.join(", ")}
|
||||
</p>
|
||||
) : 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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -26,6 +26,7 @@ export function isLayoutFinding(f: {
|
||||
f.source === "pdn_check" ||
|
||||
f.source === "af_ai_hf" ||
|
||||
f.source === "af_trace_check" ||
|
||||
f.source === "protocol_l0" ||
|
||||
rid.startsWith("PE-PLC") ||
|
||||
rid.startsWith("PE-LAY") ||
|
||||
rid.startsWith("PE-SI") ||
|
||||
@@ -50,6 +51,7 @@ export function isLayoutFinding(f: {
|
||||
rid.startsWith("PE-ANT") ||
|
||||
rid.startsWith("PE-PDN") ||
|
||||
rid.startsWith("PE-AF") ||
|
||||
rid.startsWith("PE-PRT") ||
|
||||
/^PE-BOM-01[0-4]$/.test(rid)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/** 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";
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
from backend.periscopex.protocol_recognize import (
|
||||
PACK_MACROPHASE,
|
||||
protocol_section_for_graph,
|
||||
recognize_physical_buses,
|
||||
)
|
||||
@@ -143,7 +142,6 @@ def test_empty_graph_keeps_nessun_protocollo():
|
||||
sec = protocol_section_for_graph(DesignGraph())
|
||||
assert sec.message == "nessun protocollo riconosciuto"
|
||||
assert sec.recognized_instances == []
|
||||
assert sec.macrophase == PACK_MACROPHASE
|
||||
blob = json.dumps(sec.model_dump())
|
||||
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():
|
||||
sec = protocol_section_for_graph(_usb_graph())
|
||||
assert sec.recognized_instances
|
||||
assert sec.max_level_reached is None
|
||||
blob = json.dumps(sec.model_dump())
|
||||
assert "90" not in blob
|
||||
assert "Zdiff" not in blob
|
||||
|
||||
Reference in New Issue
Block a user