"""PCB datasheet review prompt and layout neighborhood context (no auto-place).""" from __future__ import annotations import math from backend.periscopex.functional_groups import FunctionalGroupsReport from backend.periscopex.models import ComponentConstraints, DesignGraph, LayoutGraph from backend.periscopex.pcb_inventory import PcbInventoryReport from backend.periscopex.si_check import net_length_mm PCB_SYSTEM_PROMPT = """\ You are an electrical engineer reviewing a PCB layout against the IC \ datasheet knowledge already stored in the shared library extraction \ (library/extracted). Do NOT re-read or request the PDF — schematic review \ already did the deep datasheet exam once. This is an EXAM of the existing board — do not propose a new floorplan, \ do not invent millimetres, and do not write Gerbers. ### Coverage checklist (layout-only) - Use pintable, layout_rules, and abs-max from the library JSON in context. - Domains and functional groups (IC + satellites). - Placement vs numeric layout_rules (max_distance_mm, same_layer, thermal vias). - Routing: lengths, skew, stubs — length match only with datasheet millimetres. - Power copper: comment on width/thickness vs I_load only when those numbers \ are in the context. Never assume 1 oz or 50 Ω. - Thermal: copper pour / vias under the package vs θJA / layout notes in the extraction. - Kelvin/sense pins, crystal keepout — only if named in the pintable/layout_rules. - Creepage/clearance only if the extraction or IEC number is in context. ### Findings This is a **design review** of geometry vs the library JSON, not a second \ datasheet exam. Put board measurements in `finding` (FACT), library text in \ `why` (REQUIREMENT). Recommended layout notes are never ERROR. Missing \ width/thickness/I_load → say insufficient evidence, do not assume 1 oz. Every finding MUST have status ERROR, WARNING, or INFO and a non-empty \ recommendation. Call submit_review. Empty findings with checked_areas is \ valid when the layout matches the extraction. """ def format_library_extraction(cons: ComponentConstraints) -> str: """Compact shared-library dump so PCB review does not re-ingest the PDF.""" lines = [ "### Shared library extraction (library/extracted — do not re-read PDF)", f"MPN: {cons.mpn}", f"subtype: {cons.component_subtype or '—'}", f"model_version: {cons.model_version}", ] if cons.package_info: lines.append( f"package: {cons.package_info.package} " f"({cons.package_info.pin_count} pins)" ) if cons.layout_rules: lines.append("layout_rules:") for rule in cons.layout_rules[:40]: lines.append(f" {rule}") if cons.absolute_maximum_ratings: lines.append("absolute_maximum_ratings:") for r in cons.absolute_maximum_ratings[:30]: lines.append( f" {r.parameter}: min={r.min} max={r.max} {r.unit} (p.{r.source_page})" ) if cons.pintable: lines.append("pintable (number name):") for p in cons.pintable[:80]: lines.append(f" {p.number} {p.name}") return "\n".join(lines) def build_pcb_layout_context( ic_ref: str, graph: DesignGraph, layout: LayoutGraph | None, plan: FunctionalGroupsReport | None, inventory: PcbInventoryReport | None = None, ) -> str: """Plain-text block appended to the schematic neighborhood for PCB review.""" lines = ["### PCB layout context (existing board — do not move parts)"] domain_id = None group = None if plan is not None: for g in plan.groups: if g.ref == ic_ref: group = g break for d in plan.domains: if ic_ref in d.ic_refs: domain_id = d.domain_id lines.append( f"Domain: {d.domain_id} (power nets: {', '.join(d.power_nets) or '—'})" ) break if group is None: lines.append("Functional group: (this IC is not in a classified group)") else: lines.append( f"Functional group: {group.ref} subtype={group.component_subtype or '—'} " f"rank={group.rank}" ) sats = group.satellites or [] if sats: lines.append("Satellites:") for s in sats: xy = "" if layout and s.ref in layout.footprints: fp = layout.footprints[s.ref] xy = f" @ ({fp.x:.2f},{fp.y:.2f}) {fp.layer}" lines.append( f" {s.ref} role={s.role_hint} nets={','.join(s.nets or [])}{xy}" ) else: lines.append("Satellites: none") if layout and ic_ref in layout.footprints: fp = layout.footprints[ic_ref] lines.append( f"Footprint {ic_ref}: ({fp.x:.2f},{fp.y:.2f}) layer={fp.layer} " f"pads={len(fp.pads)}" ) near: list[str] = [] for other, ofp in layout.footprints.items(): if other == ic_ref: continue dist = math.hypot(ofp.x - fp.x, ofp.y - fp.y) if dist <= 15.0: near.append(f"{other} {dist:.1f}mm {ofp.layer}") if near: lines.append("Within 15 mm: " + "; ".join(sorted(near)[:24])) pin_nets = graph.components.get(ic_ref) if pin_nets: seen: set[str] = set() lines.append("Connected net lengths (PCB copper):") for net_name in pin_nets.pins.values(): if not net_name or net_name in seen: continue seen.add(net_name) mm = net_length_mm(layout, net_name) lines.append(f" {net_name}: {mm:.2f} mm") elif layout is None: lines.append("No LayoutGraph — skip millimetre claims.") else: lines.append(f"No footprint for {ic_ref} on the PCB.") if inventory and inventory.nets: sample = inventory.nets[:12] lines.append("Inventory sample (name length_mm pair):") for n in sample: lines.append( f" {n.name}: {n.length_mm:.2f} mm pair={n.pair or '—'} " f"Z0={n.z0_ohm if n.z0_ohm is not None else '—'}" ) if domain_id: lines.append(f"(domain_id={domain_id})") return "\n".join(lines)