Files
periscope/backend/services/pcb_validation.py
T
michele facbaa2305 Show Action on finding cards and inject PCB geometry into AI context.
Layout and AI findings always get action (fallback recommendation). The card
renders that sentence in the body. PCB review context now includes vias under
the footprint, copper thickness, nearby widths, courtyard, and keepout polygons.
2026-09-20 09:13:51 +02:00

107 lines
3.8 KiB
Python

"""Per-IC PCB layout review — consumes shared library extraction, no second PDF exam."""
from __future__ import annotations
import logging
from pathlib import Path
from backend.periscopex.cad_bridge import annotate_findings_cad, cad_index_from_graph
from backend.periscopex.finding_engine import complete_findings
from backend.periscopex.functional_groups import FunctionalGroupsReport
from backend.periscopex.models import ComponentType, DesignGraph, Finding, LayoutGraph
from backend.periscopex.pcb_inventory import PcbInventoryReport
from backend.periscopex.pcb_review import (
PCB_SYSTEM_PROMPT,
build_pcb_layout_context,
format_library_extraction,
)
from backend.periscopex.validate import ReviewResult, _match_constraints
from backend.services.api_logs import ApiLogger
from backend.services.validation import review_ic_async
log = logging.getLogger(__name__)
_FIX = "Adjust the layout to match the datasheet recommendation, then re-run PCB review."
def _ensure_recs(findings: list[Finding]) -> None:
for f in findings:
if f.source in (None, "review"):
f.source = "pcb_review"
rec = (f.recommendation or "").strip()
act = (f.action or "").strip()
if not rec:
rec = act or _FIX
f.recommendation = rec
if not act:
f.action = rec
complete_findings(findings)
def _has_library_extraction(cons) -> bool:
return cons is not None and bool(cons.pintable)
async def review_pcb_ics(
graph: DesignGraph,
constraints_map: dict,
layout: LayoutGraph | None,
plan: FunctionalGroupsReport | None,
inventory: PcbInventoryReport | None,
pdf_dir: Path,
storage=None,
api_logger: ApiLogger | None = None,
on_progress=None,
) -> tuple[list[Finding], dict[str, list[str]], list[dict]]:
"""Layout-only AI exam using ``library/extracted`` (or project extracted/).
Does not attach a datasheet PDF. ICs without a library pintable are skipped
with a reason to run schematic review first.
"""
findings: list[Finding] = []
coverage: dict[str, list[str]] = {}
skipped: list[dict] = []
cache: dict = {}
for ref, comp in sorted(graph.components.items()):
if comp.component_type != ComponentType.IC:
continue
mpn = (comp.mpn or "").strip() or (comp.value or "").strip()
if not mpn:
skipped.append({"designator": ref, "reason": "no MPN in BOM"})
continue
cons = _match_constraints(mpn, constraints_map)
if not _has_library_extraction(cons):
skipped.append({
"designator": ref,
"reason": "no library extraction — run schematic review first",
})
continue
extra = "\n\n".join([
format_library_extraction(cons),
build_pcb_layout_context(ref, graph, layout, plan, inventory),
])
try:
result, _trace = await review_ic_async(
graph, constraints_map, ref, None,
on_progress=on_progress,
api_logger=api_logger,
pdf_dir=pdf_dir,
storage=storage,
excerpt_cache=cache,
extra_context=extra,
system_prompt=PCB_SYSTEM_PROMPT,
log_stage="pcb_review",
)
except Exception:
log.exception("PCB AI review failed for %s — skipping", ref)
skipped.append({"designator": ref, "reason": "pcb_review error"})
continue
if not isinstance(result, ReviewResult):
continue
_ensure_recs(result.findings)
annotate_findings_cad(result.findings, cad_index_from_graph(graph))
findings.extend(result.findings)
if result.checked_areas:
coverage[ref] = list(result.checked_areas)
return findings, coverage, skipped