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.
174 lines
4.8 KiB
Python
174 lines
4.8 KiB
Python
"""Finding engine — FACT/REQUIREMENT/INFERENCE, provenance clamp, decisions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from backend.periscopex.finding_engine import (
|
|
DesignerDecision,
|
|
apply_decisions,
|
|
complete_finding,
|
|
finding_fingerprint,
|
|
lookup_rule,
|
|
)
|
|
from backend.periscopex.models import Finding
|
|
from backend.periscopex.validate import assign_finding_ids, _parse_review
|
|
|
|
|
|
def test_recommended_never_error():
|
|
f = Finding(
|
|
designator="U1",
|
|
finding="layout note",
|
|
why="place close",
|
|
status="ERROR",
|
|
rule_id="PE-THM-001",
|
|
source="pcb_power_thermal",
|
|
evidence_status="SUFFICIENT",
|
|
facts="no pour",
|
|
requirement="datasheet layout page",
|
|
)
|
|
complete_finding(f)
|
|
assert f.provenance == "RECOMMENDED"
|
|
assert f.status != "ERROR"
|
|
assert f.finding_class != "RULE"
|
|
|
|
|
|
def test_llm_review_is_never_rule():
|
|
result = _parse_review(
|
|
{
|
|
"findings": [{
|
|
"finding": "strap",
|
|
"why": "datasheet",
|
|
"status": "ERROR",
|
|
"source_page": 2,
|
|
"source_quote": "PSEL must be high for 3.3 V.",
|
|
"recommendation": "Tie PSEL high.",
|
|
}],
|
|
"checked_areas": [],
|
|
},
|
|
"U1",
|
|
"PART",
|
|
)
|
|
f = result.findings[0]
|
|
complete_finding(f)
|
|
assert f.finding_class == "REVIEW"
|
|
assert f.facts
|
|
assert f.requirement
|
|
assert f.status == "ERROR"
|
|
|
|
|
|
def test_insufficient_evidence_does_not_stay_error():
|
|
f = Finding(
|
|
designator="U1",
|
|
finding="narrow trace",
|
|
why="",
|
|
status="ERROR",
|
|
source="pcb_power_thermal",
|
|
rule_id="PE-PWR-001",
|
|
evidence_status="INSUFFICIENT",
|
|
)
|
|
complete_finding(f)
|
|
assert f.status != "ERROR"
|
|
assert "Insufficient evidence" in f.inference
|
|
assert f.finding_class != "RULE" or f.evidence_status == "INSUFFICIENT"
|
|
|
|
|
|
def test_decision_suppresses_rescan_nag():
|
|
f = Finding(
|
|
designator="U1",
|
|
finding="PSEL low",
|
|
why="must be high",
|
|
status="ERROR",
|
|
rule_id="PE-MUX-001",
|
|
source="pin_mux_check",
|
|
net="PSEL",
|
|
aspect="config",
|
|
facts="PSEL net is GND",
|
|
requirement="PSEL high for 3V3",
|
|
)
|
|
complete_finding(f)
|
|
assert f.status == "ERROR"
|
|
d = DesignerDecision(
|
|
decision_id="DEC-1",
|
|
fingerprint=finding_fingerprint(f),
|
|
rule_id="PE-MUX-001",
|
|
designator="U1",
|
|
net="PSEL",
|
|
aspect="config",
|
|
intent="wontfix",
|
|
reason="PSEL low intentional",
|
|
)
|
|
apply_decisions([f], [d])
|
|
assert f.suppressed is True
|
|
assert f.status == "INFO"
|
|
assert f.finding_class == "INFO"
|
|
assert "PSEL low intentional" in f.inference
|
|
|
|
|
|
def test_schematic_assign_ids_fills_engine_fields():
|
|
f = Finding(
|
|
designator="U3",
|
|
finding="NC tied",
|
|
why="NC must float",
|
|
status="ERROR",
|
|
rule_id="PE-NC-001",
|
|
source="nc_pin_check",
|
|
)
|
|
assign_finding_ids([f])
|
|
assert f.finding_id == "U3-001"
|
|
assert f.facts == "NC tied"
|
|
assert f.requirement
|
|
assert f.provenance == "MANDATORY"
|
|
assert f.finding_class == "RULE"
|
|
assert f.confidence is not None
|
|
|
|
|
|
def test_same_object_pcb_and_schema():
|
|
assert lookup_rule("PE-NC-001").domain == "schema"
|
|
assert lookup_rule("PE-LAY-001").domain == "pcb"
|
|
sch = Finding(designator="U1", finding="x", status="INFO", rule_id="PE-NC-001", source="nc_pin_check")
|
|
pcb = Finding(designator="U1", finding="y", status="INFO", rule_id="PE-LAY-001", source="pcb_net_match")
|
|
complete_finding(sch)
|
|
complete_finding(pcb)
|
|
for obj in (sch, pcb):
|
|
assert obj.facts
|
|
assert obj.finding_class
|
|
assert obj.provenance
|
|
assert obj.evidence_status
|
|
assert (obj.action or "").strip()
|
|
|
|
|
|
def test_empty_recommendation_gets_action():
|
|
f = Finding(designator="U1", finding="note", status="INFO", source="review")
|
|
complete_finding(f)
|
|
assert f.action.strip()
|
|
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",
|
|
finding="10n vs 100n",
|
|
why="datasheet typical 100n",
|
|
status="WARNING",
|
|
rule_id="PE-DEC-002",
|
|
source="passive_rail_check",
|
|
)
|
|
complete_finding(f)
|
|
assert f.finding_class == "REVIEW"
|
|
assert f.provenance == "RECOMMENDED"
|
|
assert f.status != "ERROR"
|