English findings-tree labels and PCB stage timings (2.84.0).

Certifications, Protocols, domains, empty states, and the protocol empty message are English. Error / Warning / Info stay. Sidebar version is stamped 2.84.0. A PCB exam records extract, pcb_checks, protocol, af, and total seconds.
This commit is contained in:
2026-09-22 22:15:23 +02:00
parent f79c722e68
commit 5ab69a835c
15 changed files with 102 additions and 34 deletions
+1 -1
View File
@@ -385,7 +385,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)
# M0 protocol cert: empty instances / "nessun protocollo riconosciuto". No Z.
# M0 protocol cert: empty instances / "No protocol recognized.". No Z.
protocol_certification: dict[str, Any] | None = None
@@ -25,7 +25,7 @@ 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"
EMPTY_PROTOCOL_MESSAGE = "No protocol recognized."
BusType = Literal[
"MEMORY",
+41 -4
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import json
import logging
import time
from datetime import datetime, timezone
from pathlib import Path
@@ -220,6 +221,14 @@ async def run_pcb_pipeline(
pcb_state=None,
)
t_total = time.perf_counter()
stage_seconds: dict[str, float] = {}
def _lap(name: str, started: float) -> float:
seconds = round(time.perf_counter() - started, 3)
stage_seconds[name] = seconds
return seconds
try:
async with PipelineWorkspace(storage, user_id, project_id) as ws:
if _cancelled(storage, user_id, project_id):
@@ -247,7 +256,10 @@ async def run_pcb_pipeline(
return
_step(project_id, "classify", "running", "domains and groups")
t_extract = time.perf_counter()
cmap = _load_constraints_map(ws.local_path("extracted"), storage)
extract_s = _lap("extract", t_extract)
_step(project_id, "extract", "complete", f"loaded stored extractions; {extract_s}s")
plan = build_placement_plan(graph, cmap)
fg_path = ws.local_path("functional_groups.json")
fg_path.write_text(plan.model_dump_json(indent=2) + "\n")
@@ -306,7 +318,9 @@ async def run_pcb_pipeline(
return
_step(project_id, "checks", "running")
t_checks = time.perf_counter()
findings = run_pcb_checks(graph, cmap, layout, plan, zrep)
checks_s = _lap("pcb_checks", t_checks)
scan_ver = app_settings.get_default_model_version()
si_skip = si_extract_needed_skips(graph, cmap, scan_ver)
sipath = ws.local_path("si_extract_needed.json")
@@ -315,7 +329,8 @@ async def run_pcb_pipeline(
_step(
project_id, "checks", "complete",
f"{len(findings)} deterministic findings"
+ (f"; {len(si_skip)} SI re-extract" if si_skip else ""),
+ (f"; {len(si_skip)} SI re-extract" if si_skip else "")
+ f"; {checks_s}s",
)
if _cancelled(storage, user_id, project_id):
@@ -323,6 +338,7 @@ async def run_pcb_pipeline(
return
_step(project_id, "af_trace", "running", "AF trigger + closed-form/cascade (not OpenEMS)")
t_af_trace = time.perf_counter()
try:
extra_af = check_af_traces(graph, cmap, layout, zrep, findings)
findings.extend(extra_af)
@@ -330,9 +346,10 @@ async def run_pcb_pipeline(
except Exception:
logger.exception("AF trace analysis failed — keeping PCB checks")
n_trace = 0
af_trace_s = _lap("af_trace", t_af_trace)
_step(
project_id, "af_trace", "complete",
f"{n_trace} AF trace findings (SI/HF checks kept)",
f"{n_trace} AF trace findings (SI/HF checks kept); {af_trace_s}s",
)
if _cancelled(storage, user_id, project_id):
@@ -340,10 +357,13 @@ async def run_pcb_pipeline(
return
_step(project_id, "af_ai", "running", "AI HF flags then deterministic investigate")
t_af_ai = time.perf_counter()
n_af = _run_af_ai_section(ws, graph, cmap, layout, zrep, findings)
af_ai_s = _lap("af_ai", t_af_ai)
stage_seconds["af"] = round(stage_seconds.get("af_trace", 0) + af_ai_s, 3)
_step(
project_id, "af_ai", "complete",
f"{n_af} extra AF+AI findings (SI/HF checks kept)",
f"{n_af} extra AF+AI findings (SI/HF checks kept); {af_ai_s}s; af={stage_seconds['af']}s",
)
if _cancelled(storage, user_id, project_id):
@@ -387,11 +407,14 @@ async def run_pcb_pipeline(
return
_step(project_id, "write_report", "running")
t_protocol = time.perf_counter()
proto_section, proto_findings = protocol_exam(
graph, layout=layout,
constraints_map=cmap, impedance_nets=zrep,
channel_data=_load_channel_data(ws),
)
protocol_s = _lap("protocol", t_protocol)
_step(project_id, "protocol", "complete", f"{protocol_s}s")
findings.extend(proto_findings)
assign_pcb_finding_ids(findings)
annotate_findings_cad(findings, cad_index_from_graph(graph))
@@ -422,10 +445,22 @@ async def run_pcb_pipeline(
ws._upload_file("periscope-findings.json")
_step(project_id, "write_report", "complete", f"{len(findings)} findings")
stage_seconds["total"] = round(time.perf_counter() - t_total, 3)
_step(
project_id, "timing", "complete",
"extract={extract}s pcb_checks={pcb_checks}s protocol={protocol}s af={af}s total={total}s".format(
extract=stage_seconds.get("extract"),
pcb_checks=stage_seconds.get("pcb_checks"),
protocol=stage_seconds.get("protocol"),
af=stage_seconds.get("af"),
total=stage_seconds.get("total"),
),
)
_publish(project_id, "pcb_complete", {
"findings": len(findings),
"domains": len(plan.domains),
"groups": len(plan.groups),
"stage_seconds": stage_seconds,
})
proj_svc.update_project(
storage, user_id, project_id,
@@ -435,15 +470,17 @@ async def run_pcb_pipeline(
"domains": len(plan.domains),
"groups": len(plan.groups),
"skipped": len(skipped),
"stage_seconds": stage_seconds,
},
pcb_cancel_requested=False,
)
except Exception as e:
logger.exception("pcb pipeline failed for %s", project_id)
stage_seconds["total"] = round(time.perf_counter() - t_total, 3)
proj_svc.update_project(
storage, user_id, project_id,
pcb_status="error",
pcb_state={"error": str(e)},
pcb_state={"error": str(e), "stage_seconds": stage_seconds},
)
_publish(project_id, "pcb_error", {"error": str(e)})
@@ -14,6 +14,7 @@ PE-AF-002 on an Ethernet MDI pair compares one calculated pair Z to the packed c
- [Changed] `bom_pcb_check.py`, `esd_return_check.py`, `protocol_l0.py`, `af_trace_check.py`.
- [Changed] pytest `tests/pcb/test_bom_esd_ep_cc_eth.py`.
- [Changed] Findings tree labels are English: domains, Certifications, Protocols, actions, and empty states. Error / Warning / Info stay.
## 2.83.0 — 2026-09-22 — Apple-like shell; USB-C vSafe still UNKNOWN
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "periscope-web",
"version": "2.83.0",
"version": "2.84.0",
"private": true,
"scripts": {
"sync-version": "node scripts/sync-version.mjs",
@@ -343,9 +343,9 @@ function ReportContent({ projectId }: { projectId: string }) {
{report.summary.total === 0 &&
!(report.protocol_certification?.recognized_instances?.length) ? (
<div className="rounded-[11px] border border-black/10 px-5 py-10 text-center">
<p className="text-[15px] font-medium tracking-tight">Nessun finding</p>
<p className="text-[15px] font-medium tracking-tight">No findings</p>
<p className="mt-1 text-[13px] text-neutral-500">
Carica i datasheet degli IC sulla pagina progetto e rilancia lesame.
Upload the IC datasheets on the project page and run the exam again.
</p>
</div>
) : (
@@ -161,7 +161,7 @@ type NavItem =
const PRIMARY_NAV: NavItem[] = [
{ type: "tab", tab: "bom", label: "Esame", icon: ScanLine },
{ type: "route", path: "/report", label: "Protocolli", icon: BookOpen },
{ type: "route", path: "/report", label: "Protocols", icon: BookOpen },
{ type: "route", path: "/report", label: "Report", icon: ClipboardList },
];
@@ -216,7 +216,7 @@ function ProjectNav({
const onReport = pathname === `${base}/report`;
function isActive(item: NavItem): boolean {
if (item.label === "Protocolli") return onReport;
if (item.label === "Protocols") return onReport;
if (item.label === "Report") return onReport;
if (item.type === "route") {
return pathname === `${base}${item.path}` && !currentTab;
@@ -228,7 +228,7 @@ function ProjectNav({
}
function getHref(item: NavItem): string {
if (item.label === "Protocolli") return `${base}/report`;
if (item.label === "Protocols") return `${base}/report`;
if (item.type === "route") return `${base}${item.path}`;
return `${base}?tab=${item.tab}`;
}
@@ -19,6 +19,7 @@ import {
forestHasCertifications,
groupByWorstStatus,
isProtocolFinding,
protocolEmptyMessage,
protocolInstancesFromSection,
protocolInstancesForStatus,
worstStatus,
@@ -267,9 +268,9 @@ function CertificationsBranch({
} & FindingsTreeProps) {
const all = [...findings, ...instances.map(() => ({ status }))];
return (
<TreeBranch label="Certificazioni" findings={all.length ? all : [{ status }]} defaultOpen>
<TreeBranch label="Certifications" findings={all.length ? all : [{ status }]} defaultOpen>
<TreeBranch
label="Protocolli"
label="Protocols"
findings={all.length ? all : [{ status }]}
defaultOpen
>
@@ -318,7 +319,7 @@ function CertificationsBranch({
) : null}
{instances.length === 0 && findings.length === 0 ? (
<p className="px-2 py-2 text-[13px] text-neutral-500">
{props.protocolSection?.message || "Nessun protocollo riconosciuto."}
{protocolEmptyMessage(props.protocolSection?.message)}
</p>
) : null}
</TreeBranch>
@@ -329,9 +330,9 @@ function CertificationsBranch({
export function FindingsTreeEmpty() {
return (
<div className="rounded-[11px] border border-black/10 px-5 py-10 text-center dark:border-white/10">
<p className="text-[15px] font-medium tracking-tight">Nessun finding visibile</p>
<p className="text-[15px] font-medium tracking-tight">No visible findings</p>
<p className="mt-1 text-[13px] text-neutral-500">
I filtri nascondono lelenco. Togli un filtro, oppure apri i findings già rivisti sotto.
Filters are hiding the list. Clear a filter, or open the reviewed findings below.
</p>
</div>
);
@@ -10,6 +10,7 @@ import {
type ProtocolReportCheck,
} from "@/lib/protocol-report";
import {
protocolEmptyMessage,
protocolInstancesFromSection,
type ProtocolTreeInstance,
} from "@/lib/findings-forest";
@@ -28,7 +29,7 @@ export function ProtocolInstanceBody({ instance }: { instance: ProtocolTreeInsta
{instance.worst ? ` · ${instance.worst}` : ""}
</p>
{fail ? (
<p className="text-[11px] text-[rgb(180,40,35)]">FAIL in testa non sotto Warning.</p>
<p className="text-[11px] text-[rgb(180,40,35)]">FAIL is listed first not under Warning.</p>
) : null}
<div className="overflow-x-auto">
<table className="w-full text-[11px] tabular-nums">
@@ -57,7 +58,7 @@ export function ProtocolInstanceBody({ instance }: { instance: ProtocolTreeInsta
</table>
</div>
{instance.chainExample ? (
<p className="break-all text-[11px] text-neutral-500">Catena: {instance.chainExample}</p>
<p className="break-all text-[11px] text-neutral-500">Chain: {instance.chainExample}</p>
) : null}
{checks
.filter((c) => c.result === "FAIL" || c.skip_visible)
@@ -113,12 +114,12 @@ export function ProtocolSection({
section?: ProtocolCertificationSection | null;
}) {
const instances = protocolInstancesFromSection(section);
const message = section?.message || "Nessun protocollo riconosciuto.";
const message = protocolEmptyMessage(section?.message);
const maxLevel = section?.max_level_reached || "—";
return (
<section className="space-y-2">
<p className="text-[11px] text-neutral-500">
Livello massimo {maxLevel} (L0L3). HDMI 1.4 TMDS resta UNKNOWN niente Zdiff inventato.
Max level {maxLevel} (L0L3). HDMI 1.4 TMDS stays UNKNOWN no invented Zdiff.
</p>
{instances.length === 0 ? (
<p className="text-[13px] text-neutral-600">{message}</p>
@@ -20,18 +20,18 @@ export function ReportSummary({
const infos = summary.INFO ?? 0;
const total = summary.total || errors + warnings + infos;
const outcome =
errors > 0 ? "Error" : warnings > 0 ? "Attenzione" : total > 0 ? "Completato" : "Senza findings";
errors > 0 ? "Error" : warnings > 0 ? "Warning" : total > 0 ? "Complete" : "No findings";
const bits = [
`${errors} Error`,
`${warnings} Warning`,
`${infos} Info`,
`${reviewedCount} rivisti`,
`${reviewedCount} reviewed`,
];
if (typeof totalCostUsd === "number" && totalCostUsd > 0) {
bits.push(`$${totalCostUsd.toFixed(2)}`);
}
if (typeof creditsSpent === "number" && creditsSpent > 0) {
bits.push(`${creditsSpent.toFixed(2)} crediti`);
bits.push(`${creditsSpent.toFixed(2)} credits`);
}
return (
@@ -1,6 +1,13 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { groupByWorstStatus, groupDomains, isProtocolFinding, worstStatus } from "./findings-forest";
import {
groupByWorstStatus,
groupDomains,
isProtocolFinding,
protocolEmptyMessage,
ruleDomain,
worstStatus,
} from "./findings-forest";
import { isPcbExamFinding } from "./layout-finding";
import type { Finding } from "./types";
@@ -72,6 +79,16 @@ test("Error folder lists domains and rules after status, protocol stays out of t
assert.equal(groupDomains(groups[0].findings.filter((x) => !isProtocolFinding(x))).length, 1);
});
test("protocol folder label is Protocols, including a stored Italian empty message", () => {
const domain = ruleDomain(
f({ designator: "J2", status: "ERROR", rule_id: "PE-PRT-L0-001", source: "protocol_l0" }),
);
assert.equal(domain.label, "Protocols");
assert.equal(protocolEmptyMessage("nessun protocollo riconosciuto"), "No protocol recognized.");
assert.equal(protocolEmptyMessage(""), "No protocol recognized.");
assert.equal(protocolEmptyMessage("USB2 recognized"), "USB2 recognized");
});
test("PE-PLC stays in the PCB exam bucket with PE-BOM", () => {
assert.equal(
isPcbExamFinding({ source: "placement_check", rule_id: "PE-PLC-002" }),
@@ -1,4 +1,4 @@
/** Group report findings for the tree: status folders, then domains/rules, then Certificazioni. */
/** Group report findings for the tree: status folders, then domains/rules, then Certifications. */
import type { Finding, FindingStatus, ProtocolCertificationSection } from "./types";
import {
@@ -45,7 +45,7 @@ const DOMAIN_LABEL: Record<string, string> = {
PDN: "PDN",
PD: "USB-PD",
AF: "AF Board + AI",
PRT: "Protocolli",
PRT: "Protocols",
MUX: "Pin mux",
NC: "NC pins",
DEC: "Decoupling",
@@ -112,6 +112,17 @@ export function groupByDesignator(findings: Finding[]): DesignatorGroup[] {
.map(([designator, items]) => ({ designator, findings: items }));
}
export const PROTOCOL_EMPTY_MESSAGE = "No protocol recognized.";
/** Tree copy. Stored 2.83 reports still carry the Italian empty phrase. */
export function protocolEmptyMessage(message?: string | null): string {
const text = (message || "").trim();
if (!text || text.toLowerCase() === "nessun protocollo riconosciuto") {
return PROTOCOL_EMPTY_MESSAGE;
}
return text;
}
export function isProtocolFinding(f: {
source?: string | null;
rule_id?: string | null;
@@ -122,7 +133,7 @@ export function isProtocolFinding(f: {
}
export function ruleDomain(f: Finding): { id: string; label: string } {
if (isProtocolFinding(f)) return { id: "PRT", label: "Protocolli" };
if (isProtocolFinding(f)) return { id: "PRT", label: "Protocols" };
const rid = f.rule_id || "";
const m = rid.match(/^PE-([A-Z]+)/);
if (m) {
@@ -173,7 +184,7 @@ export function groupDomains(findings: Finding[]): DomainGroup[] {
/**
* Error / Warning / Info folders. A ref lives in the folder of its worst child.
* U18 with PE-PLC-002 ERROR + five WARNING → Error, badge ERROR, count 6.
* Protocol findings (PE-PRT) are excluded from domain folders — they sit under Certificazioni.
* Protocol findings (PE-PRT) are excluded from domain folders — they sit under Certifications.
*/
export function groupByWorstStatus(findings: Finding[]): StatusGroup[] {
const buckets: Record<FindingStatus, DesignatorGroup[]> = {
+1 -1
View File
@@ -1,3 +1,3 @@
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
export const APP_VERSION = "2.83.0";
export const APP_VERSION = "2.84.0";
export const APP_VERSION_DATE = "2026-09-22";
+2 -2
View File
@@ -223,7 +223,7 @@ def test_reject_same_logical_and_physical_id():
def test_empty_report_section_has_no_z():
section = empty_protocol_section()
dumped = section.model_dump()
assert section.message == "nessun protocollo riconosciuto"
assert section.message == "No protocol recognized."
assert section.recognized_instances == []
assert dumped.get("max_level_reached") is None
blob = json.dumps(dumped)
@@ -238,7 +238,7 @@ def test_empty_report_section_has_no_z():
summary={"ERROR": 0, "WARNING": 0, "INFO": 0},
protocol_certification=dumped,
)
assert report.protocol_certification["message"] == "nessun protocollo riconosciuto"
assert report.protocol_certification["message"] == "No protocol recognized."
def test_pcie_cxl_physical_packs_are_empty_numbers():
+2 -2
View File
@@ -138,9 +138,9 @@ def test_axi4_internal_does_not_create_pcb_nets():
assert "AXI_WDATA" not in one.nets
def test_empty_graph_keeps_nessun_protocollo():
def test_empty_graph_keeps_no_protocol_message():
sec = protocol_section_for_graph(DesignGraph())
assert sec.message == "nessun protocollo riconosciuto"
assert sec.message == "No protocol recognized."
assert sec.recognized_instances == []
blob = json.dumps(sec.model_dump())
assert "ohm" not in blob.lower()