From 8b4f7710cffdc27e2b1888278382f08625079905 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Tue, 22 Sep 2026 01:04:08 +0200 Subject: [PATCH] Show Error groups with max child severity (2.63.4). PCB tree folders are Error / Warning / Info. U18 with PE-PLC-002 ERROR is ERROR, not WARNING. J2 and other connectors list with ICs. PE-PLC stays in the PCB exam bucket. No deploy. --- periscope/src/frontend/content/changelog.md | 8 + periscope/src/frontend/package.json | 5 +- .../src/components/report/findings-tree.tsx | 138 +++++++++--------- .../frontend/src/lib/findings-forest.test.ts | 62 ++++++++ .../src/frontend/src/lib/findings-forest.ts | 70 +++++++++ .../src/frontend/src/lib/layout-finding.ts | 31 +--- periscope/src/frontend/src/lib/version.ts | 4 +- 7 files changed, 212 insertions(+), 106 deletions(-) create mode 100644 periscope/src/frontend/src/lib/findings-forest.test.ts create mode 100644 periscope/src/frontend/src/lib/findings-forest.ts diff --git a/periscope/src/frontend/content/changelog.md b/periscope/src/frontend/content/changelog.md index 20a9e65..374b0e9 100644 --- a/periscope/src/frontend/content/changelog.md +++ b/periscope/src/frontend/content/changelog.md @@ -2,6 +2,14 @@ What's new in Periscope. +## 2.63.4 — 2026-09-22 — Report Error group, J2, U18 max severity + +The findings tree always has an **Error** folder when ERROR findings are visible. Group badge is the worst child (U18 PE-PLC-002 ERROR is not labeled WARNING). Connectors (J2) sit in the same sidebar as U*. PE-PLC stays in the PCB exam bucket. No auth change. + +- [Fixed] Status folders Error / Warning / Info; severity = max of children. +- [Fixed] J2 and other non-U refs listed with ICs. +- [Fixed] `isPcbExamFinding` matches layout findings (PE-PLC with PE-BOM). + ## 2.63.3 — 2026-09-22 — Module die, truncated QFN pintable, QFN=VQFN HubAudio software leftovers on PE-BOM-010/011: a WROOM module is not the ESP32 die pintable; a QFN-48 land is not wrong because extraction stopped at pins 1–24; VQFN-24 4×4 is the same QFN family as datasheet QFN-24. Extra signal pads above the declared QFN-n still ERROR. Pad ≠ via. Kelvin unchanged. diff --git a/periscope/src/frontend/package.json b/periscope/src/frontend/package.json index 133e47e..5ce20f5 100644 --- a/periscope/src/frontend/package.json +++ b/periscope/src/frontend/package.json @@ -1,6 +1,6 @@ { "name": "periscope-web", - "version": "2.63.1", + "version": "2.63.4", "private": true, "scripts": { "sync-version": "node scripts/sync-version.mjs", @@ -9,7 +9,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test": "node --experimental-strip-types --test src/lib/findings-forest.test.ts" }, "dependencies": { "@base-ui/react": "^1.3.0", diff --git a/periscope/src/frontend/src/components/report/findings-tree.tsx b/periscope/src/frontend/src/components/report/findings-tree.tsx index d47df23..08946f6 100644 --- a/periscope/src/frontend/src/components/report/findings-tree.tsx +++ b/periscope/src/frontend/src/components/report/findings-tree.tsx @@ -9,13 +9,13 @@ import type { Finding, FindingComment, FindingReview, - FindingStatus, Collaborator, Component, DesignGraph, } from "@/lib/types"; import { cn, sortFindings, subtypeLabel } from "@/lib/utils"; import { isAfAiFinding, isPcbExamFinding } from "@/lib/layout-finding"; +import { groupByWorstStatus, worstStatus } from "@/lib/findings-forest"; interface FindingsTreeProps { findings: Finding[]; @@ -37,12 +37,6 @@ interface FindingsTreeProps { onReviewSaved?: (findingId: string, review: FindingReview) => void; } -function worstStatus(findings: Finding[]): FindingStatus { - if (findings.some((f) => f.status === "ERROR")) return "ERROR"; - if (findings.some((f) => f.status === "WARNING")) return "WARNING"; - return "INFO"; -} - function pinNetLabel(f: Finding): string { const pin = (f.pins && f.pins[0]) || ""; const net = f.net || ""; @@ -131,76 +125,74 @@ function DesignatorForest({ graph, ...cardProps }: FindingsTreeProps) { - const byDes = new Map(); - for (const f of sortFindings(findings)) { - const list = byDes.get(f.designator) ?? []; - list.push(f); - byDes.set(f.designator, list); - } - const designators = [...byDes.keys()].sort((a, b) => { - const wa = worstStatus(byDes.get(a)!); - const wb = worstStatus(byDes.get(b)!); - const order = { ERROR: 0, WARNING: 1, INFO: 2 } as const; - return order[wa] - order[wb] || a.localeCompare(b); - }); + const folders = groupByWorstStatus(findings); return (
- {designators.map((d) => { - const group = byDes.get(d)!; - const component: Component | undefined = graph.components[d]; - const extra = [ - component?.mpn, - component?.component_subtype ? subtypeLabel(component.component_subtype) : "", - ] - .filter(Boolean) - .join(" · "); - const byLeaf = new Map(); - for (const f of group) { - const label = pinNetLabel(f); - const list = byLeaf.get(label) ?? []; - list.push(f); - byLeaf.set(label, list); - } - const leaves = [...byLeaf.entries()].sort((a, b) => a[0].localeCompare(b[0])); - return ( - - {leaves.map(([label, items]) => ( - -
- {sortFindings(items).map((f) => { - const key = cardProps.findingKeys.get(f); - return ( - cardProps.onToggleReviewed!(key) - : undefined - } - comments={f.finding_id ? cardProps.comments?.[f.finding_id] : undefined} - projectId={cardProps.projectId} - collaborators={cardProps.collaborators} - currentUserId={cardProps.currentUserId} - currentUserName={cardProps.currentUserName} - onCommentAdded={cardProps.onCommentAdded} - onCommentDeleted={cardProps.onCommentDeleted} - onReportFinding={cardProps.onReportFinding} - isReported={!!(f.finding_id && cardProps.reportedFindingIds?.has(f.finding_id))} - review={f.finding_id ? cardProps.reviews?.[f.finding_id] : undefined} - onReviewSaved={cardProps.onReviewSaved} - /> - ); - })} -
+ {folders.map((folder) => ( + + {folder.designators.map((row) => { + const d = row.designator; + const group = row.findings; + const component: Component | undefined = graph.components[d]; + const extra = [ + component?.mpn, + component?.component_subtype ? subtypeLabel(component.component_subtype) : "", + ] + .filter(Boolean) + .join(" · "); + const byLeaf = new Map(); + for (const finding of group) { + const label = pinNetLabel(finding); + const list = byLeaf.get(label) ?? []; + list.push(finding); + byLeaf.set(label, list); + } + const leaves = [...byLeaf.entries()].sort((a, b) => a[0].localeCompare(b[0])); + return ( + + {leaves.map(([label, items]) => ( + +
+ {sortFindings(items).map((item) => { + const key = cardProps.findingKeys.get(item); + return ( + cardProps.onToggleReviewed!(key) + : undefined + } + comments={item.finding_id ? cardProps.comments?.[item.finding_id] : undefined} + projectId={cardProps.projectId} + collaborators={cardProps.collaborators} + currentUserId={cardProps.currentUserId} + currentUserName={cardProps.currentUserName} + onCommentAdded={cardProps.onCommentAdded} + onCommentDeleted={cardProps.onCommentDeleted} + onReportFinding={cardProps.onReportFinding} + isReported={!!(item.finding_id && cardProps.reportedFindingIds?.has(item.finding_id))} + review={item.finding_id ? cardProps.reviews?.[item.finding_id] : undefined} + onReviewSaved={cardProps.onReviewSaved} + /> + ); + })} +
+
+ ))}
- ))} -
- ); - })} + ); + })} +
+ ))}
); } diff --git a/periscope/src/frontend/src/lib/findings-forest.test.ts b/periscope/src/frontend/src/lib/findings-forest.test.ts new file mode 100644 index 0000000..6794bc8 --- /dev/null +++ b/periscope/src/frontend/src/lib/findings-forest.test.ts @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { groupByWorstStatus, worstStatus } from "./findings-forest.ts"; +import { isPcbExamFinding } from "./layout-finding.ts"; +import type { Finding } from "./types.ts"; + +function f(partial: Partial & Pick): Finding { + return { + finding_id: null, + mpn: "", + aspect: null, + finding: partial.finding ?? "", + why: "", + source_page: null, + reference: "", + ...partial, + }; +} + +test("U18 worst status is ERROR when PE-PLC-002 is inside", () => { + const items = [ + f({ designator: "U18", status: "ERROR", rule_id: "PE-PLC-002" }), + f({ designator: "U18", status: "WARNING" }), + f({ designator: "U18", status: "WARNING" }), + f({ designator: "U18", status: "WARNING" }), + f({ designator: "U18", status: "WARNING" }), + f({ designator: "U18", status: "WARNING" }), + ]; + assert.equal(worstStatus(items), "ERROR"); + const groups = groupByWorstStatus(items); + assert.deepEqual( + groups.map((g) => g.label), + ["Error"], + ); + assert.equal(groups[0].designators[0].designator, "U18"); + assert.equal(groups[0].findings.length, 6); + assert.equal(groups[0].status, "ERROR"); +}); + +test("Error folder lists J2 connectors, not only U*", () => { + const groups = groupByWorstStatus([ + f({ designator: "J2", status: "ERROR", rule_id: "PE-BOM-011" }), + f({ designator: "U11", status: "ERROR" }), + f({ designator: "U5", status: "WARNING" }), + ]); + assert.equal(groups[0].label, "Error"); + const refs = groups[0].designators.map((d) => d.designator); + assert.deepEqual(refs, ["J2", "U11"]); + assert.ok(!refs.every((r) => r.startsWith("U"))); + assert.equal(groups[1].label, "Warning"); +}); + +test("PE-PLC stays in the PCB exam bucket with PE-BOM", () => { + assert.equal( + isPcbExamFinding({ source: "placement_check", rule_id: "PE-PLC-002" }), + true, + ); + assert.equal( + isPcbExamFinding({ source: "bom_pcb_check", rule_id: "PE-BOM-011" }), + true, + ); +}); diff --git a/periscope/src/frontend/src/lib/findings-forest.ts b/periscope/src/frontend/src/lib/findings-forest.ts new file mode 100644 index 0000000..3c36750 --- /dev/null +++ b/periscope/src/frontend/src/lib/findings-forest.ts @@ -0,0 +1,70 @@ +/** Group report findings for the tree: status folders, then every ref (J2 included). */ + +import type { Finding, FindingStatus } from "./types"; + +const RANK: Record = { ERROR: 0, WARNING: 1, INFO: 2 }; + +export const STATUS_FOLDER: Record = { + ERROR: "Error", + WARNING: "Warning", + INFO: "Info", +}; + +export function worstStatus(findings: { status: FindingStatus }[]): FindingStatus { + if (findings.some((f) => f.status === "ERROR")) return "ERROR"; + if (findings.some((f) => f.status === "WARNING")) return "WARNING"; + return "INFO"; +} + +export type DesignatorGroup = { + designator: string; + findings: Finding[]; +}; + +export type StatusGroup = { + status: FindingStatus; + label: string; + findings: Finding[]; + designators: DesignatorGroup[]; +}; + +/** Sidebar rows: ICs, connectors (J*), and any other ref. Never U-only. */ +export function groupByDesignator(findings: Finding[]): DesignatorGroup[] { + const byDes = new Map(); + for (const f of findings) { + const d = (f.designator || "").trim() || "(no ref)"; + const list = byDes.get(d) ?? []; + list.push(f); + byDes.set(d, list); + } + return [...byDes.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([designator, items]) => ({ designator, findings: items })); +} + +/** + * 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. + */ +export function groupByWorstStatus(findings: Finding[]): StatusGroup[] { + const buckets: Record = { + ERROR: [], + WARNING: [], + INFO: [], + }; + for (const row of groupByDesignator(findings)) { + buckets[worstStatus(row.findings)].push(row); + } + const order: FindingStatus[] = ["ERROR", "WARNING", "INFO"]; + return order + .filter((status) => buckets[status].length > 0) + .map((status) => { + const designators = buckets[status]; + return { + status, + label: STATUS_FOLDER[status], + findings: designators.flatMap((d) => d.findings), + designators, + }; + }); +} diff --git a/periscope/src/frontend/src/lib/layout-finding.ts b/periscope/src/frontend/src/lib/layout-finding.ts index 6632cbf..c5dd8a2 100644 --- a/periscope/src/frontend/src/lib/layout-finding.ts +++ b/periscope/src/frontend/src/lib/layout-finding.ts @@ -54,38 +54,11 @@ export function isLayoutFinding(f: { } export function isPcbExamFinding(f: { + finding_id?: string | null; source?: string | null; rule_id?: string | null; }): boolean { - const rid = f.rule_id || ""; - return ( - f.source === "pcb_review" || - f.source === "pcb_power_thermal" || - f.source === "hierarchy_check" || - f.source === "timing_check" || - f.source === "pi_check" || - f.source === "esd_return_check" || - f.source === "bom_pcb_check" || - f.source === "spof_check" || - f.source === "emi_check" || - rid.startsWith("PE-PWR") || - rid.startsWith("PE-THM") || - rid.startsWith("PE-VIA") || - rid.startsWith("PE-KEL") || - rid.startsWith("PE-STCH") || - rid.startsWith("PE-HIER") || - rid.startsWith("PE-TIM") || - rid.startsWith("PE-PI") || - rid.startsWith("PE-ESD") || - rid.startsWith("PE-RET") || - rid.startsWith("PE-SPOF") || - rid.startsWith("PE-EMI") || - rid.startsWith("PE-STK") || - rid.startsWith("PE-PD-") || - rid.startsWith("PE-ANT") || - rid.startsWith("PE-PDN") || - /^PE-BOM-01[0-4]$/.test(rid) - ); + return isLayoutFinding(f) && !isAfAiFinding(f); } export function isAfAiFinding(f: { diff --git a/periscope/src/frontend/src/lib/version.ts b/periscope/src/frontend/src/lib/version.ts index aa5a91a..1aff00c 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.63.1"; -export const APP_VERSION_DATE = "2026-09-21"; +export const APP_VERSION = "2.63.4"; +export const APP_VERSION_DATE = "2026-09-22";