Add MODE=pcb layout exam job with AI and deterministic checks.

Parallel pcb_status pipeline: parse board, classify domains/groups,
inventory traces, PE-LAY/PLC/SI/DRT checks, per-IC datasheet review.
Findings merge into the report UI. No auto-place or pcbnew write-back.
This commit is contained in:
2026-09-19 22:54:17 +02:00
parent a2011dad91
commit 16c606ae3d
28 changed files with 1906 additions and 32 deletions
+11 -22
View File
@@ -26,7 +26,6 @@ from backend.periscopex.models import (
ComponentType,
DesignGraph,
Finding,
LayoutGraph,
NetType,
ValidationReport,
)
@@ -61,8 +60,6 @@ from backend.periscopex.dnp_check import check_dnp_enables
from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir
from backend.periscopex.errata_check import check_errata
from backend.periscopex.internal_features_check import check_internal_features
from backend.periscopex.placement_check import check_placement
from backend.periscopex.si_check import check_si
from backend.periscopex.crystal_cl_check import check_crystal_cl
from backend.periscopex.nc_pin_check import check_nc_pins
@@ -77,7 +74,6 @@ def _is_deterministic(f: Finding) -> bool:
def _run_deterministic_checks(
graph: DesignGraph, constraints_map: dict,
lifecycle_map: dict | None = None,
layout: LayoutGraph | None = None,
) -> list[Finding]:
"""Run the deterministic graph checks, fail-soft per check — a check bug
can never break the review or the report."""
@@ -100,8 +96,6 @@ def _run_deterministic_checks(
("lifecycle_check", lambda: check_lifecycle(graph, lifecycle_map)),
("errata_check", lambda: check_errata(graph, constraints_map)),
("internal_features_check", lambda: check_internal_features(graph, constraints_map)),
("placement_check", lambda: check_placement(graph, constraints_map, layout)),
("si_check", lambda: check_si(graph, constraints_map, layout)),
("crystal_cl_check", lambda: check_crystal_cl(graph)),
("nc_pin_check", lambda: check_nc_pins(graph, constraints_map)),
):
@@ -112,17 +106,6 @@ def _run_deterministic_checks(
return out
def _load_layout_graph(graph_path: str) -> LayoutGraph | None:
path = Path(graph_path).with_name("layout_graph.json")
if not path.is_file():
return None
try:
return LayoutGraph.model_validate_json(path.read_text())
except Exception:
log.exception("layout_graph.json invalid — skipping placement_check")
return None
def _assistant_text(blocks) -> str:
"""Best-effort extraction of text content from a completion's raw
assistant blocks. Provider-agnostic and never raises."""
@@ -312,6 +295,9 @@ async def review_ic_async(
pdf_dir: Path | None = None,
storage=None,
excerpt_cache: dict | None = None,
extra_context: str = "",
system_prompt: str | None = None,
log_stage: str = "review",
) -> tuple[ReviewResult, dict]:
"""Review one IC against its datasheet. Async, multi-turn.
@@ -373,7 +359,7 @@ async def review_ic_async(
session = await provider.create_session(
model=model,
system=SYSTEM_PROMPT,
system=system_prompt or SYSTEM_PROMPT,
# Gemini 2.5/3 thinking models count thoughts against this cap.
# 4096 was too tight: U3 (largest IC) burned the entire budget
# on thinking and emitted zero visible output, dropping its
@@ -387,13 +373,16 @@ async def review_ic_async(
)
try:
context = build_component_context(graph, constraints_map, ic_ref)
user_text = f"Review this component's usage:\n\n{context}"
if extra_context.strip():
user_text += "\n\n" + extra_context.strip()
initial_msg = Message(
role="user",
content=[
PdfBlock(path=Path(trimmed_pdf), cacheable=True),
TextBlock(
text=f"Review this component's usage:\n\n{context}",
text=user_text,
cacheable=True,
),
],
@@ -503,7 +492,7 @@ async def review_ic_async(
)
if api_logger:
api_logger.log(
stage="review", identifier=ic_ref,
stage=log_stage, identifier=ic_ref,
model=model, provider=provider.name,
input_tokens=total_input, output_tokens=total_output,
cache_creation_input_tokens=total_cache_creation,
@@ -607,7 +596,7 @@ async def review_ic_async(
trace["duration_ms"] = int((time.monotonic() - t0) * 1000)
if api_logger:
api_logger.log(
stage="review", identifier=ic_ref,
stage=log_stage, identifier=ic_ref,
model=model, provider=provider.name,
input_tokens=total_input, output_tokens=total_output,
cache_creation_input_tokens=total_cache_creation,
@@ -730,7 +719,7 @@ async def validate_design_async(
if loaded:
lifecycle_map.update(loaded)
deterministic_findings = _run_deterministic_checks(
graph, constraints_map, lifecycle_map, layout=_load_layout_graph(graph_path),
graph, constraints_map, lifecycle_map,
)
pdf_dir_path = Path(pdf_dir)