Apply the finding engine to schematic and PCB reviews.

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.
This commit is contained in:
2026-09-20 08:38:59 +02:00
parent 8f4ebc645c
commit dc65e96a22
16 changed files with 797 additions and 211 deletions
+47 -1
View File
@@ -11,6 +11,12 @@ from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from pydantic import BaseModel
from backend.periscopex.finding_engine import (
apply_decisions,
complete_findings,
decision_from_review,
upsert_decision,
)
from backend.periscopex.models import Finding
from backend.periscopex.review_workflow import (
ReviewError,
@@ -49,6 +55,20 @@ async def get_report(project_id: str, request: Request):
merged = merge_schema_pcb_reports(schema, pcb)
if merged is None:
raise HTTPException(404, "Report not found — run the pipeline first")
findings = _findings_from_report(merged)
complete_findings(findings)
dec_key = f"{prefix}/decisions.json"
if storage.exists(dec_key):
try:
apply_decisions(findings, storage.read_json(dec_key) or [])
except Exception:
pass
merged["findings"] = [json.loads(f.model_dump_json()) for f in findings]
summary = {"ERROR": 0, "WARNING": 0, "INFO": 0, "total": len(findings)}
for f in findings:
if f.status in summary:
summary[f.status] += 1
merged["summary"] = summary
return JSONResponse(merged)
@@ -148,7 +168,17 @@ async def put_finding_review(project_id: str, finding_id: str, body: ReviewBody,
owner_user_id, _ = await resolve_or_404(request, project_id)
user_id = get_user_id(request)
key, report_data = _load_report(storage, owner_user_id, project_id)
ids = {f.finding_id for f in _findings_from_report(report_data) if f.finding_id}
prefix = proj_svc.project_prefix(owner_user_id, project_id)
findings = _findings_from_report(report_data)
ids = {f.finding_id for f in findings if f.finding_id}
if finding_id not in ids:
pcb_key = f"{prefix}/pcb_report.json"
if storage.exists(pcb_key):
pcb_data = storage.read_json(pcb_key)
pcb_findings = _findings_from_report(pcb_data)
if finding_id in {f.finding_id for f in pcb_findings if f.finding_id}:
key, report_data, findings = pcb_key, pcb_data, pcb_findings
ids = {f.finding_id for f in findings if f.finding_id}
if finding_id not in ids:
raise HTTPException(404, "Finding not found")
try:
@@ -164,6 +194,22 @@ async def put_finding_review(project_id: str, finding_id: str, body: ReviewBody,
raise HTTPException(400, str(exc)) from exc
report_data["review_states"] = states
storage.write_json(key, report_data)
if body.state in {"wontfix", "false_positive"}:
found = next(
(f for f in _findings_from_report(report_data) if f.finding_id == finding_id),
None,
)
if found is not None:
dec = decision_from_review(
found, state=body.state, reason=body.reason, user_id=user_id,
)
if dec is not None:
prefix = proj_svc.project_prefix(owner_user_id, project_id)
dkey = f"{prefix}/decisions.json"
existing = storage.read_json(dkey) if storage.exists(dkey) else []
if not isinstance(existing, list):
existing = []
storage.write_json(dkey, upsert_decision(existing, dec))
return JSONResponse(states.get(finding_id) or {"state": "open", "reason": ""})