Register library components from a datasheet without an exam (2.63.1).
POST /api/library/datasheets now writes an IC inbox card or a passive/discrete model. Empty pintables never enter library/extracted. AF Board+AI runs after unchanged run_pcb_checks (PE-SI / HF line stay).
This commit is contained in:
@@ -5,7 +5,12 @@ Does not replace run_pcb_checks. Not DRC. Never invents Z.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.periscopex.af_ai_hf import HfHypothesis, investigate_hf_hypotheses
|
||||
from backend.periscopex.af_ai_hf import (
|
||||
HfHypothesis,
|
||||
append_investigated,
|
||||
hypotheses_from_rows,
|
||||
investigate_hf_hypotheses,
|
||||
)
|
||||
from backend.periscopex.hf_line_check import check_hf_lines
|
||||
from backend.periscopex.models import (
|
||||
Component,
|
||||
@@ -144,3 +149,57 @@ def test_z0_with_stackup_does_not_invent_ohm_number():
|
||||
for f in out:
|
||||
assert "50 Ω" not in (f.finding or "")
|
||||
assert "90 Ω" not in (f.finding or "")
|
||||
|
||||
|
||||
def test_append_investigated_keeps_pcb_checks_and_adds_pe_af():
|
||||
graph = _usb_graph()
|
||||
layout = _stub_layout()
|
||||
cons = _stub_constraints()
|
||||
pcb = run_pcb_checks(graph, cons, layout)
|
||||
n_si = sum(1 for f in pcb if f.rule_id == "PE-SI-007")
|
||||
assert n_si >= 1
|
||||
flags = [
|
||||
HfHypothesis(net="USB_D+", issue_class="stub", why="looks long"),
|
||||
HfHypothesis(net="USB_D+", issue_class="z0", why="probably 90 ohm"),
|
||||
]
|
||||
added = append_investigated(pcb, graph, cons, layout, flags)
|
||||
assert any(f.rule_id == "PE-AF-001" for f in added)
|
||||
assert sum(1 for f in pcb if f.rule_id == "PE-SI-007") == n_si
|
||||
assert "si_check" in _pcb_checks_source()
|
||||
assert "hf_line" in _pcb_checks_source()
|
||||
|
||||
|
||||
def test_hypotheses_from_rows_drops_unknown_class():
|
||||
rows = [
|
||||
{"net": "USB_D+", "issue_class": "stub", "why": "x"},
|
||||
{"net": "USB_D+", "issue_class": "clearance", "why": "drc"},
|
||||
{"net": "", "issue_class": "z0", "why": "empty"},
|
||||
]
|
||||
hyps = hypotheses_from_rows(rows)
|
||||
assert len(hyps) == 1
|
||||
assert hyps[0].issue_class == "stub"
|
||||
|
||||
|
||||
def test_pcb_pipeline_af_ai_is_after_run_pcb_checks():
|
||||
import inspect
|
||||
|
||||
from backend.periscopex import pcb_checks
|
||||
from backend.services import pcb_pipeline
|
||||
|
||||
src = inspect.getsource(pcb_pipeline.run_pcb_pipeline)
|
||||
assert "run_pcb_checks" in src
|
||||
assert "_run_af_ai_section" in src
|
||||
assert src.find("run_pcb_checks") < src.find("_run_af_ai_section")
|
||||
checks = inspect.getsource(pcb_checks.run_pcb_checks)
|
||||
assert '("si_check"' in checks or '"si_check"' in checks
|
||||
assert '"hf_line"' in checks
|
||||
assert "af_ai" not in checks
|
||||
assert "investigate_hf_hypotheses" not in checks
|
||||
|
||||
|
||||
def _pcb_checks_source() -> str:
|
||||
import inspect
|
||||
|
||||
from backend.periscopex.pcb_checks import run_pcb_checks
|
||||
|
||||
return inspect.getsource(run_pcb_checks)
|
||||
|
||||
@@ -137,19 +137,33 @@ def test_library_import_pdf_without_project(tmp_path):
|
||||
before = client.get("/api/projects").json()
|
||||
resp = client.post(
|
||||
"/api/library/datasheets",
|
||||
data={"mpn": "CH340E"},
|
||||
data={"mpn": "CH340E", "kind": "ic"},
|
||||
files={"file": ("ch.pdf", PDF, "application/pdf")},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["mpn"] == "CH340E"
|
||||
assert body["has_extraction"] is False
|
||||
assert body["component_type"] == "ic"
|
||||
assert body["status"] == "needs_pintable"
|
||||
assert client.get("/api/projects").json() == before
|
||||
catalog = client.get("/api/library").json()
|
||||
assert any(d["mpn"] == "CH340E" for d in catalog["datasheets"])
|
||||
assert any(d["mpn"] == "CH340E" and d.get("has_component") for d in catalog["datasheets"])
|
||||
ics = [c for c in catalog["ics"] if c["mpn"] == "CH340E"]
|
||||
assert len(ics) == 1
|
||||
assert ics[0]["pin_count"] == 0
|
||||
assert ics[0]["library_status"] == "needs_pintable"
|
||||
pdf = client.get("/api/library/datasheet/CH340E")
|
||||
assert pdf.status_code == 200
|
||||
assert pdf.content.startswith(b"%PDF-")
|
||||
card = client.get("/api/library/components/ic/CH340E")
|
||||
assert card.status_code == 200
|
||||
assert card.json()["status"] == "needs_pintable"
|
||||
from backend.main import app
|
||||
from backend.services.library import library_has_extraction
|
||||
|
||||
assert library_has_extraction(app.state.storage, "CH340E") is None
|
||||
assert not app.state.storage.exists("library/extracted/CH340E.json")
|
||||
|
||||
|
||||
def test_library_import_rejects_non_pdf(tmp_path):
|
||||
@@ -210,3 +224,63 @@ def test_library_put_rejects_empty_pintable(tmp_path):
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert client.get("/api/library/components/ic/FAKEIC").status_code == 404
|
||||
|
||||
|
||||
def test_library_import_ic_put_pintable_promotes_out_of_inbox(tmp_path):
|
||||
client = _client(tmp_path)
|
||||
assert client.post(
|
||||
"/api/library/datasheets",
|
||||
data={"mpn": "CH340E", "kind": "ic"},
|
||||
files={"file": ("ch.pdf", PDF, "application/pdf")},
|
||||
).status_code == 200
|
||||
payload = {
|
||||
"mpn": "CH340E",
|
||||
"component_subtype": "ic.interface.usb_uart_bridge",
|
||||
"pintable": [
|
||||
{"number": "1", "name": "VCC"},
|
||||
{"number": "2", "name": "GND"},
|
||||
],
|
||||
"absolute_maximum_ratings": [],
|
||||
"rules": [],
|
||||
}
|
||||
saved = client.put("/api/library/components/ic/CH340E", json=payload)
|
||||
assert saved.status_code == 200
|
||||
from backend.main import app
|
||||
|
||||
assert app.state.storage.exists("library/extracted/CH340E.json")
|
||||
assert not app.state.storage.exists("library/inbox/CH340E.json")
|
||||
extracted = app.state.storage.read_json("library/extracted/CH340E.json")
|
||||
assert "status" not in extracted
|
||||
assert extracted["pintable"][0]["name"] == "VCC"
|
||||
cat = client.get("/api/library").json()
|
||||
ic = next(c for c in cat["ics"] if c["mpn"] == "CH340E")
|
||||
assert ic["pin_count"] == 2
|
||||
assert ic["library_status"] == "extracted"
|
||||
|
||||
|
||||
def test_library_import_passive_without_exam(tmp_path):
|
||||
client = _client(tmp_path)
|
||||
resp = client.post(
|
||||
"/api/library/datasheets",
|
||||
data={"mpn": "CL10B474KA8NNNC", "kind": "passive_part"},
|
||||
files={"file": ("c.pdf", PDF, "application/pdf")},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["component_type"] == "passive_part"
|
||||
assert client.get("/api/projects").json() == []
|
||||
cat = client.get("/api/library").json()
|
||||
assert any(p["mpn"] == "CL10B474KA8NNNC" for p in cat["passive_parts"])
|
||||
got = client.get("/api/library/components/passive_part/CL10B474KA8NNNC")
|
||||
assert got.status_code == 200
|
||||
assert got.json()["specs"]["specs_type"] == "passive"
|
||||
|
||||
|
||||
def test_library_import_rejects_unknown_kind(tmp_path):
|
||||
client = _client(tmp_path)
|
||||
resp = client.post(
|
||||
"/api/library/datasheets",
|
||||
data={"mpn": "CH340E", "kind": "project"},
|
||||
files={"file": ("ch.pdf", PDF, "application/pdf")},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert client.get("/api/library").json()["ics"] == []
|
||||
|
||||
Reference in New Issue
Block a user