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.
107 lines
3.8 KiB
Python
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
|