Gate PCB SI not_reviewed to board ICs, not the shared library.

Emmaforo listed 20 stale 1.5–1.7 MPNs (LAN8720A, W25Q, …) as
not reviewed while the four BOM ICs were already extracted and reviewed.
This commit is contained in:
2026-09-20 14:05:03 +02:00
parent 312057b6d3
commit 0a75e5971a
3 changed files with 103 additions and 23 deletions
+51 -23
View File
@@ -23,7 +23,9 @@ from backend.periscopex.finding_engine import apply_decisions
from backend.periscopex.functional_groups import build_placement_plan
from backend.periscopex.graph import build_graph
from backend.periscopex.layout_rules import needs_layout_rules_refresh
from backend.periscopex.models import ComponentConstraints, DesignGraph, LayoutGraph, ValidationReport
from backend.periscopex.models import (
ComponentConstraints, ComponentType, DesignGraph, LayoutGraph, ValidationReport,
)
from backend.periscopex.pcb_checks import assign_pcb_finding_ids, run_pcb_checks
from backend.periscopex.pcb_inventory import build_pcb_inventory
from backend.services import projects as proj_svc
@@ -104,6 +106,50 @@ def _load_constraints_map(
return result
def si_extract_needed_skips(
graph: DesignGraph,
cmap: dict,
scan_ver: str,
) -> list[dict]:
"""Stale SI extracts for *board* ICs only — not the shared library dump.
``cmap`` includes every ``library/extracted`` JSON. Warning those MPNs
as ``not_reviewed`` on an unrelated project (Emmaforo vs LAN8720A) is
a false banner.
"""
board_mpns: set[str] = set()
refs_by_mpn: dict[str, list[str]] = {}
for ref, comp in graph.components.items():
if comp.component_type != ComponentType.IC:
continue
mpn = (comp.mpn or comp.value or "").strip()
if not mpn:
continue
board_mpns.add(mpn)
refs_by_mpn.setdefault(mpn, []).append(ref)
out: list[dict] = []
for mpn, cons in sorted(cmap.items()):
if mpn not in board_mpns:
continue
payload = {
"model_version": cons.model_version,
"layout_rules": cons.layout_rules,
}
if not needs_layout_rules_refresh(payload, min_scan_version=scan_ver):
continue
refs = ",".join(sorted(refs_by_mpn.get(mpn, []))) or mpn
out.append({
"designator": refs,
"reason": (
f"SI layout_rules empty at model_version="
f"{cons.model_version}; re-run schematic review "
f"to re-extract pintable {scan_ver} "
"(PCB does not re-read the PDF)."
),
})
return out
def _publish(project_id: str, event: str, data: dict) -> None:
broker.publish(project_id, event, data)
@@ -222,29 +268,11 @@ async def run_pcb_pipeline(
_step(project_id, "checks", "running")
findings = run_pcb_checks(graph, cmap, layout, plan, zrep)
si_skip: list[dict] = []
scan_ver = app_settings.get_default_model_version()
for mpn, cons in sorted(cmap.items()):
payload = {
"model_version": cons.model_version,
"layout_rules": cons.layout_rules,
}
if needs_layout_rules_refresh(
payload, min_scan_version=scan_ver,
):
si_skip.append({
"designator": mpn,
"reason": (
f"SI layout_rules empty at model_version="
f"{cons.model_version}; re-run schematic review "
f"to re-extract pintable {scan_ver} "
"(PCB does not re-read the PDF)."
),
})
if si_skip:
sipath = ws.local_path("si_extract_needed.json")
sipath.write_text(json.dumps(si_skip, indent=2) + "\n")
ws._upload_file("si_extract_needed.json")
si_skip = si_extract_needed_skips(graph, cmap, scan_ver)
sipath = ws.local_path("si_extract_needed.json")
sipath.write_text(json.dumps(si_skip, indent=2) + "\n")
ws._upload_file("si_extract_needed.json")
_step(
project_id, "checks", "complete",
f"{len(findings)} deterministic findings"