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:
@@ -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",
|
||||
|
||||
@@ -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,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 l’esame.
|
||||
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 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.
|
||||
</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} (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.
|
||||
</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,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";
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user