From 5ab69a835c2b18f17b489be60029323f0d286a59 Mon Sep 17 00:00:00 2001
From: Michele Bigi
Date: Tue, 22 Sep 2026 22:15:23 +0200
Subject: [PATCH] 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.
---
periscope/src/backend/periscopex/models.py | 2 +-
.../backend/periscopex/protocol_catalog.py | 2 +-
.../src/backend/services/pcb_pipeline.py | 45 +++++++++++++++++--
periscope/src/frontend/content/changelog.md | 1 +
periscope/src/frontend/package.json | 2 +-
.../app/(app)/project/[id]/report/page.tsx | 4 +-
.../src/components/layout/sidebar.tsx | 6 +--
.../src/components/report/findings-tree.tsx | 11 ++---
.../components/report/protocol-section.tsx | 9 ++--
.../src/components/report/report-summary.tsx | 6 +--
.../frontend/src/lib/findings-forest.test.ts | 19 +++++++-
.../src/frontend/src/lib/findings-forest.ts | 19 ++++++--
periscope/src/frontend/src/lib/version.ts | 2 +-
tests/pcb/test_protocol_catalog_m0.py | 4 +-
tests/pcb/test_protocol_recognize_m1.py | 4 +-
15 files changed, 102 insertions(+), 34 deletions(-)
diff --git a/periscope/src/backend/periscopex/models.py b/periscope/src/backend/periscopex/models.py
index 66f8897..fb7fdf9 100644
--- a/periscope/src/backend/periscopex/models.py
+++ b/periscope/src/backend/periscopex/models.py
@@ -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
diff --git a/periscope/src/backend/periscopex/protocol_catalog.py b/periscope/src/backend/periscopex/protocol_catalog.py
index 5ee5e95..6072456 100644
--- a/periscope/src/backend/periscopex/protocol_catalog.py
+++ b/periscope/src/backend/periscopex/protocol_catalog.py
@@ -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",
diff --git a/periscope/src/backend/services/pcb_pipeline.py b/periscope/src/backend/services/pcb_pipeline.py
index 98fca49..b78dc6b 100644
--- a/periscope/src/backend/services/pcb_pipeline.py
+++ b/periscope/src/backend/services/pcb_pipeline.py
@@ -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)})
diff --git a/periscope/src/frontend/content/changelog.md b/periscope/src/frontend/content/changelog.md
index 4aefd20..65aa38e 100644
--- a/periscope/src/frontend/content/changelog.md
+++ b/periscope/src/frontend/content/changelog.md
@@ -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
diff --git a/periscope/src/frontend/package.json b/periscope/src/frontend/package.json
index 39764ac..290489c 100644
--- a/periscope/src/frontend/package.json
+++ b/periscope/src/frontend/package.json
@@ -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",
diff --git a/periscope/src/frontend/src/app/(app)/project/[id]/report/page.tsx b/periscope/src/frontend/src/app/(app)/project/[id]/report/page.tsx
index 7b63c5e..80f596c 100644
--- a/periscope/src/frontend/src/app/(app)/project/[id]/report/page.tsx
+++ b/periscope/src/frontend/src/app/(app)/project/[id]/report/page.tsx
@@ -343,9 +343,9 @@ function ReportContent({ projectId }: { projectId: string }) {
{report.summary.total === 0 &&
!(report.protocol_certification?.recognized_instances?.length) ? (
-
Nessun finding
+
No findings
- Carica i datasheet degli IC sulla pagina progetto e rilancia l’esame.
+ Upload the IC datasheets on the project page and run the exam again.
) : (
diff --git a/periscope/src/frontend/src/components/layout/sidebar.tsx b/periscope/src/frontend/src/components/layout/sidebar.tsx
index b79891b..260a5c2 100644
--- a/periscope/src/frontend/src/components/layout/sidebar.tsx
+++ b/periscope/src/frontend/src/components/layout/sidebar.tsx
@@ -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}`;
}
diff --git a/periscope/src/frontend/src/components/report/findings-tree.tsx b/periscope/src/frontend/src/components/report/findings-tree.tsx
index e23a970..9cc6e30 100644
--- a/periscope/src/frontend/src/components/report/findings-tree.tsx
+++ b/periscope/src/frontend/src/components/report/findings-tree.tsx
@@ -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 (
-
+
@@ -318,7 +319,7 @@ function CertificationsBranch({
) : null}
{instances.length === 0 && findings.length === 0 ? (
- {props.protocolSection?.message || "Nessun protocollo riconosciuto."}
+ {protocolEmptyMessage(props.protocolSection?.message)}
) : null}
@@ -329,9 +330,9 @@ function CertificationsBranch({
export function FindingsTreeEmpty() {
return (
-
Nessun finding visibile
+
No visible findings
- I filtri nascondono l’elenco. Togli un filtro, oppure apri i findings già rivisti sotto.
+ Filters are hiding the list. Clear a filter, or open the reviewed findings below.
);
diff --git a/periscope/src/frontend/src/components/report/protocol-section.tsx b/periscope/src/frontend/src/components/report/protocol-section.tsx
index e9732f6..22eb2e6 100644
--- a/periscope/src/frontend/src/components/report/protocol-section.tsx
+++ b/periscope/src/frontend/src/components/report/protocol-section.tsx
@@ -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}` : ""}
{fail ? (
- FAIL in testa — non sotto Warning.
+ FAIL is listed first — not under Warning.
) : null}
@@ -57,7 +58,7 @@ export function ProtocolInstanceBody({ instance }: { instance: ProtocolTreeInsta
{instance.chainExample ? (
- Catena: {instance.chainExample}
+ Chain: {instance.chainExample}
) : 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 (
- Livello massimo {maxLevel} (L0–L3). HDMI 1.4 TMDS resta UNKNOWN — niente Zdiff inventato.
+ Max level {maxLevel} (L0–L3). HDMI 1.4 TMDS stays UNKNOWN — no invented Zdiff.
{instances.length === 0 ? (
{message}
diff --git a/periscope/src/frontend/src/components/report/report-summary.tsx b/periscope/src/frontend/src/components/report/report-summary.tsx
index e1683e6..2c340aa 100644
--- a/periscope/src/frontend/src/components/report/report-summary.tsx
+++ b/periscope/src/frontend/src/components/report/report-summary.tsx
@@ -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 (
diff --git a/periscope/src/frontend/src/lib/findings-forest.test.ts b/periscope/src/frontend/src/lib/findings-forest.test.ts
index 93c3c76..e870cf0 100644
--- a/periscope/src/frontend/src/lib/findings-forest.test.ts
+++ b/periscope/src/frontend/src/lib/findings-forest.test.ts
@@ -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" }),
diff --git a/periscope/src/frontend/src/lib/findings-forest.ts b/periscope/src/frontend/src/lib/findings-forest.ts
index 36d7037..4826a68 100644
--- a/periscope/src/frontend/src/lib/findings-forest.ts
+++ b/periscope/src/frontend/src/lib/findings-forest.ts
@@ -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 = {
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 = {
diff --git a/periscope/src/frontend/src/lib/version.ts b/periscope/src/frontend/src/lib/version.ts
index 101fa68..94820d8 100644
--- a/periscope/src/frontend/src/lib/version.ts
+++ b/periscope/src/frontend/src/lib/version.ts
@@ -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";
diff --git a/tests/pcb/test_protocol_catalog_m0.py b/tests/pcb/test_protocol_catalog_m0.py
index bb33f42..d7c01b2 100644
--- a/tests/pcb/test_protocol_catalog_m0.py
+++ b/tests/pcb/test_protocol_catalog_m0.py
@@ -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():
diff --git a/tests/pcb/test_protocol_recognize_m1.py b/tests/pcb/test_protocol_recognize_m1.py
index a0409c4..85fbd0d 100644
--- a/tests/pcb/test_protocol_recognize_m1.py
+++ b/tests/pcb/test_protocol_recognize_m1.py
@@ -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()