FACT, REQUIREMENT, and INFERENCE are separate fields; datasheet provenance lives in the rule DB so Recommended cannot stay ERROR; LLM output is REVIEW. Designer decisions persist across rescans. Schematic and PCB share one library and the same finding object in the report UI.
136 lines
3.8 KiB
Python
136 lines
3.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
|