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.
This commit is contained in:
@@ -143,6 +143,21 @@ def test_empty_recommendation_gets_action():
|
||||
assert f.recommendation.strip() == f.action.strip()
|
||||
|
||||
|
||||
def test_pcb_review_ai_finding_action_without_recommendation():
|
||||
f = Finding(
|
||||
designator="U1",
|
||||
finding="PowerPAD vias",
|
||||
why="thermal pad",
|
||||
status="INFO",
|
||||
source="pcb_review",
|
||||
recommendation="",
|
||||
action="",
|
||||
)
|
||||
complete_finding(f)
|
||||
assert f.action.strip()
|
||||
assert f.finding_class == "REVIEW"
|
||||
|
||||
|
||||
def test_recommended_rc_cap_is_review_not_rule():
|
||||
f = Finding(
|
||||
designator="C1",
|
||||
|
||||
@@ -76,6 +76,23 @@ def test_kicad_sch_is_rejected_as_pcb(tmp_path: Path):
|
||||
parse_kicad_pcb(p)
|
||||
|
||||
|
||||
def test_parse_keepout_zone_polygon(tmp_path: Path):
|
||||
p = tmp_path / "keepout.kicad_pcb"
|
||||
p.write_text("""(kicad_pcb (version 20240108) (generator pcbnew)
|
||||
(zone (net 0) (net_name "") (layer "F.Cu") (name "ANT_KEEPOUT")
|
||||
(keepout (tracks not_allowed) (vias not_allowed) (copperpour not_allowed))
|
||||
(polygon (pts (xy 0 0) (xy 10 0) (xy 10 8) (xy 0 8)))
|
||||
)
|
||||
)
|
||||
""")
|
||||
g = parse_kicad_pcb(p)
|
||||
assert len(g.zones) == 1
|
||||
z = g.zones[0]
|
||||
assert z.keepout is True
|
||||
assert z.name == "ANT_KEEPOUT"
|
||||
assert len(z.outlines[0]) == 4
|
||||
|
||||
|
||||
def test_empty_board_parses(tmp_path: Path):
|
||||
p = tmp_path / "empty.kicad_pcb"
|
||||
p.write_text("(kicad_pcb (version 20240108) (generator pcbnew))\n")
|
||||
|
||||
@@ -208,6 +208,101 @@ def test_inventory_lists_net_length_and_pair():
|
||||
assert names["USB_DP"].pair == "USB_DM"
|
||||
|
||||
|
||||
def test_layout_context_includes_via_counts():
|
||||
from backend.periscopex.models import LayoutStackup, LayoutVia, LayoutZone
|
||||
|
||||
graph = _graph()
|
||||
plan = FunctionalGroupsReport(
|
||||
domains=[PlacementDomain(domain_id="3v3", power_nets=["+3V3"], ic_refs=["U3"])],
|
||||
groups=[PlacementIcGroup(ref="U3", satellites=[])],
|
||||
)
|
||||
layout = LayoutGraph(
|
||||
stackup=LayoutStackup(
|
||||
copper_layers=["F.Cu", "B.Cu"],
|
||||
dielectrics=[],
|
||||
copper_thickness_mm=0.035,
|
||||
),
|
||||
footprints={
|
||||
"U3": LayoutFootprint(
|
||||
reference="U3", x=0, y=0, layer="F.Cu",
|
||||
courtyard=[(-2, -2), (2, -2), (2, 2), (-2, 2)],
|
||||
pads=[LayoutPad(number="1", x=0, y=0, net="+3V3")],
|
||||
),
|
||||
},
|
||||
vias=[
|
||||
LayoutVia(x=0.2, y=0.1, net="GND", drill=0.3),
|
||||
LayoutVia(x=-0.4, y=0.0, net="GND", drill=0.3),
|
||||
],
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(5, 0), width=0.45, layer="F.Cu", net="+3V3"),
|
||||
],
|
||||
zones=[
|
||||
LayoutZone(
|
||||
net="",
|
||||
layer="F.Cu",
|
||||
keepout=True,
|
||||
name="ANT_KEEPOUT",
|
||||
outlines=[[(20, 20), (30, 20), (30, 30), (20, 30)]],
|
||||
),
|
||||
],
|
||||
)
|
||||
text = build_pcb_layout_context("U3", graph, layout, plan)
|
||||
assert "Vias under footprint: 2" in text
|
||||
assert "count=2" in text
|
||||
assert "0.3 mm" in text
|
||||
assert "Copper thickness: 35 µm" in text
|
||||
assert "width=0.45 mm" in text
|
||||
assert "ANT_KEEPOUT" in text
|
||||
assert "Board keepout polygons" in text
|
||||
|
||||
|
||||
def test_pcb_ai_finding_gets_action():
|
||||
from backend.periscopex.finding_engine import complete_finding
|
||||
from backend.periscopex.models import Finding
|
||||
from backend.services.pcb_validation import _ensure_recs
|
||||
|
||||
f = Finding(
|
||||
designator="U1",
|
||||
mpn="PADIC",
|
||||
finding="PowerPAD under U1",
|
||||
why="datasheet thermal pad",
|
||||
status="INFO",
|
||||
recommendation="",
|
||||
action="",
|
||||
source="pcb_review",
|
||||
)
|
||||
_ensure_recs([f])
|
||||
complete_finding(f)
|
||||
assert f.action.strip()
|
||||
assert f.recommendation.strip()
|
||||
assert f.finding_class == "REVIEW"
|
||||
assert f.facts
|
||||
assert f.requirement
|
||||
|
||||
|
||||
def test_parse_review_action_field_without_recommendation():
|
||||
from backend.periscopex.validate import _parse_review
|
||||
|
||||
result = _parse_review(
|
||||
{
|
||||
"findings": [{
|
||||
"finding": "EPAD vias present",
|
||||
"why": "layout note",
|
||||
"status": "INFO",
|
||||
"source_page": 12,
|
||||
"source_quote": "Connect EPAD with vias to GND.",
|
||||
"action": "Keep the nine 0.3 mm vias under U5 EPAD.",
|
||||
}],
|
||||
"checked_areas": ["thermal"],
|
||||
},
|
||||
"U5",
|
||||
"ESP",
|
||||
)
|
||||
f = result.findings[0]
|
||||
assert f.action == "Keep the nine 0.3 mm vias under U5 EPAD."
|
||||
assert f.recommendation == "Keep the nine 0.3 mm vias under U5 EPAD."
|
||||
|
||||
|
||||
def test_layout_context_includes_domain_and_group():
|
||||
graph = _graph()
|
||||
plan = FunctionalGroupsReport(
|
||||
@@ -221,6 +316,7 @@ def test_layout_context_includes_domain_and_group():
|
||||
assert "Domain: 3v3" in text
|
||||
assert "Functional group: U3" in text
|
||||
assert "Footprint U3" in text
|
||||
assert "Vias under footprint" in text
|
||||
|
||||
|
||||
def test_merge_reports_prefixes_do_not_collide():
|
||||
|
||||
Reference in New Issue
Block a user