Native DeepSeek review loop in periscope/src (Fase C2).

Live review_ic_async comes from review_session; PinScope validate.py and
validation_tools.py stay in dependency as _inherited_review_ic_async fallback.
This commit is contained in:
2026-09-20 15:14:21 +02:00
parent a0a2f5bdbf
commit 974923b2e6
11 changed files with 2506 additions and 5 deletions
+115
View File
@@ -0,0 +1,115 @@
"""Prove native review parse/tools match inherited PinScope before switching the live loop."""
from __future__ import annotations
import json
from backend.periscopex.constraints_lookup import match_constraints
from backend.periscopex.finding_engine import complete_finding
from backend.periscopex.models import DesignGraph
from backend.periscopex.review_parse import parse_submit_review
from backend.periscopex.validate import _load_datasheets, _match_constraints, _parse_review
from backend.periscopex.validation_tools import execute_tool as inherited_execute
from backend.periscopex.review_tools import execute_tool as native_execute
from tests.paths import SIMPLE_PROJECT
_PAYLOAD = {
"findings": [
{
"finding": "FB5 floating",
"why": "FB5 is NC; DCDC5 SW is loaded.",
"status": "ERROR",
"source_page": 12,
"source_quote": "Connect FB5 to the output sense node.",
"recommendation": "Connect FB5.",
},
{
"finding": "no quote",
"why": "maybe missing cap",
"status": "ERROR",
"source_page": 3,
"source_quote": "",
},
],
"checked_areas": ["power", "decoupling"],
}
def test_native_parse_matches_inherited():
inherited = _parse_review(_PAYLOAD, "U16", "AXP2101")
native = parse_submit_review(_PAYLOAD, "U16", "AXP2101")
assert len(native.findings) == len(inherited.findings)
assert native.checked_areas == inherited.checked_areas
for a, b in zip(native.findings, inherited.findings, strict=True):
assert a.model_dump() == b.model_dump()
def test_native_llm_finding_is_review_and_recommended_not_error():
native = parse_submit_review(_PAYLOAD, "U16", "AXP2101")
f = native.findings[0]
complete_finding(f)
assert f.finding_class == "REVIEW"
assert f.facts == "FB5 floating"
assert f.requirement
assert f.status != "ERROR"
f2 = native.findings[1]
complete_finding(f2)
assert f2.status != "ERROR"
assert f2.why.startswith("Unverified:")
def test_match_constraints_matches_inherited():
datasheets = _load_datasheets(SIMPLE_PROJECT / "extracted")
if not datasheets:
datasheets = _load_datasheets(SIMPLE_PROJECT / "datasheets" / "extracted")
mpns = list(datasheets)[:5] or ["MSPM0G3507SPTR"]
for mpn in mpns:
assert match_constraints(mpn, datasheets) == _match_constraints(mpn, datasheets)
assert match_constraints(None, datasheets) is None
def test_native_graph_tools_match_inherited_on_simple_project():
graph = DesignGraph.model_validate(
json.loads((SIMPLE_PROJECT / "design_graph.json").read_text())
)
cmap = {}
native_txt, _ = native_execute(
graph, cmap, "get_net_for_pin", {"designator": "U3", "pin": "1"},
)
inherited_txt, _ = inherited_execute(
graph, cmap, "get_net_for_pin", {"designator": "U3", "pin": "1"},
)
assert native_txt == inherited_txt
n2, _ = native_execute(
graph, cmap, "find_connected_components",
{"designator": "U3", "pin": "1", "designator_filter": "C"},
)
i2, _ = inherited_execute(
graph, cmap, "find_connected_components",
{"designator": "U3", "pin": "1", "designator_filter": "C"},
)
assert n2 == i2
def test_native_session_does_not_import_pinscope_loop():
import ast
from pathlib import Path
import backend.services.review_session as rs
tree = ast.parse(Path(rs.__file__).read_text())
imported = [
node.module
for node in ast.walk(tree)
if isinstance(node, ast.ImportFrom) and node.module
]
assert "backend.periscopex.validate" not in imported
assert "backend.periscopex.validation_tools" not in imported
assert "backend.services.llm" in imported
assert "backend.periscopex.review_tools" in imported
assert "backend.periscopex.review_parse" in imported
from backend.services import validation as val
import backend.services.review_session as rs
assert val.review_ic_async is rs.review_ic_async
@@ -18,6 +18,7 @@ import pytest
from pypdf import PdfWriter
from backend.periscopex.utils import safe_mpn
from backend.services import review_session as review_session
from backend.services import validation as val
from backend.services.llm.types import Completion, ToolCall, Usage
from backend.services.storage import LocalStorageBackend
@@ -124,6 +125,7 @@ async def test_no_tool_calls_triggers_forced_submit_next_turn(workspace, monkeyp
return await body(_Provider(), "fake-model")
monkeypatch.setattr(val, "call_with_fallback", fake_cwf)
monkeypatch.setattr(review_session, "call_with_fallback", fake_cwf)
orig = val.review_ic_async
+2
View File
@@ -18,6 +18,7 @@ import pytest
from pypdf import PdfWriter
from backend.periscopex.utils import safe_mpn
from backend.services import review_session as review_session
from backend.services import validation as val
from backend.services.llm.types import Completion, ToolCall, Usage
from backend.services.storage import LocalStorageBackend
@@ -136,6 +137,7 @@ async def _run(ws, monkeypatch, before_ic):
return await body(prov_cls(), "fake-model")
monkeypatch.setattr(val, "call_with_fallback", fake_cwf)
monkeypatch.setattr(review_session, "call_with_fallback", fake_cwf)
# The fake session needs the current ic_ref; thread it through by wrapping
# review_ic_async to set the class attr before delegating.