diff --git a/periscope/src/backend/periscopex/parsers.py b/periscope/src/backend/periscopex/parsers.py index 0f25ca0..8b3f574 100644 --- a/periscope/src/backend/periscopex/parsers.py +++ b/periscope/src/backend/periscopex/parsers.py @@ -16,6 +16,69 @@ NetlistFormat = Literal["pads", "edif", "kicad_xml", "kicad_sexp", "kicad_sch"] _GND_EXACT = {"GND", "AGND", "DGND", "PGND", "VSS", "0V"} _DNP_TRUE = {"1", "y", "yes", "true", "dnp", "dni", "x"} _FITTED_FALSE = {"0", "n", "no", "false"} +# KiCad exports often label the part column PNM (not MPN). +_MPN_ALIASES = ( + "PNM", + "MPN", + "Manufacturer Part Number", + "Manufacturer Part", + "Part Number", + "Mfr. Part", + "Mfg Part #", +) + + +def _row_get(row: dict[str, str | None], *names: str) -> str: + keys = {str(k).lower(): k for k in row if k is not None} + for name in names: + key = keys.get(name.lower()) + if key is None: + continue + val = (row.get(key) or "").strip() + if val: + return val + return "" + + +def resolve_row_mpn( + row: dict[str, str | None], + refs: list[str], + mpn_col: str = "Manufacturer Part Number", +) -> str | None: + """PNM/MPN aliases, then IC Value when the part column is blank. Never invent.""" + mpn = _row_get(row, mpn_col, *_MPN_ALIASES) + if mpn: + return mpn + if any(re.match(r"^U\d", r, re.I) for r in refs): + value = _row_get(row, "Value", "Comment") + if value: + return value + return None + + +def ic_mpn_skip_reason( + ref: str, + *, + mpn: str | None, + value: str | None, + footprint: str | None = None, + in_bom: bool, + bom_value: str | None = None, +) -> str | None: + """Progress/not_reviewed text when an IC has no part number evidence.""" + got = (mpn or "").strip() or (value or "").strip() + if got: + return None + fp = (footprint or "").strip() or "empty" + if not in_bom: + return ( + f"INSUFFICIENT_EVIDENCE: {ref} not in BOM " + f"(Value={(value or '').strip() or 'empty'}; Footprint={fp})" + ) + return ( + f"INSUFFICIENT_EVIDENCE: {ref} BOM PNM/MPN empty " + f"(Value={bom_value!r})" + ) def detect_netlist_format(content: bytes | str) -> NetlistFormat: @@ -180,17 +243,13 @@ def parse_bom( has_variant_col = bool(colnames & {"variant"}) for row in reader: - refs_raw = row.get(reference_col, "") - value = row.get("Value", "") or row.get("Comment", "") - footprint = row.get("Footprint", "") - mpn = (row.get(mpn_col, "") or "").strip() or None - lcsc = row.get("LCSC", "") or None - datasheet_url = (row.get("Datasheet", "") or "").strip() or None + refs_raw = _row_get(row, reference_col) or (row.get(reference_col, "") or "") + value = _row_get(row, "Value", "Comment") + footprint = _row_get(row, "Footprint") refs = [r.strip() for r in refs_raw.split(",") if r.strip()] - if not mpn: - mpn = (row.get("PNM", "") or "").strip() or None - if not mpn and any(re.match(r"^U\d", r, re.I) for r in refs): - mpn = (value or "").strip() or None + mpn = resolve_row_mpn(row, refs, mpn_col=mpn_col) + lcsc = _row_get(row, "LCSC") or None + datasheet_url = _row_get(row, "Datasheet") or None dnp_raw = (row.get("DNP") or row.get("DNI") or "").strip().lower() fitted_raw = (row.get("Fitted") or row.get("Populate") or "").strip().lower() variant = (row.get("Variant") or row.get("variant") or "").strip() or None diff --git a/periscope/src/backend/services/pcb_validation.py b/periscope/src/backend/services/pcb_validation.py index 5011d02..71b6b51 100644 --- a/periscope/src/backend/services/pcb_validation.py +++ b/periscope/src/backend/services/pcb_validation.py @@ -9,6 +9,7 @@ from backend.periscopex.cad_bridge import annotate_findings_cad, cad_index_from_ from backend.periscopex.finding_engine import complete_findings from backend.periscopex.functional_groups import FunctionalGroupsReport from backend.periscopex.models import ComponentType, DesignGraph, Finding, LayoutGraph +from backend.periscopex.parsers import ic_mpn_skip_reason from backend.periscopex.pcb_inventory import PcbInventoryReport from backend.periscopex.pcb_review import ( PCB_SYSTEM_PROMPT, @@ -67,8 +68,16 @@ async def review_pcb_ics( if comp.component_type != ComponentType.IC: continue mpn = (comp.mpn or "").strip() or (comp.value or "").strip() - if not mpn: - skipped.append({"designator": ref, "reason": "no MPN in BOM"}) + skip = ic_mpn_skip_reason( + ref, + mpn=comp.mpn, + value=comp.value, + footprint=comp.footprint, + in_bom=ref in (graph.bom_fields or {}), + bom_value=(graph.bom_fields or {}).get(ref, {}).get("value"), + ) + if skip: + skipped.append({"designator": ref, "reason": skip}) continue cons = _match_constraints(mpn, constraints_map) if not _has_library_extraction(cons): diff --git a/periscope/src/backend/services/validation.py b/periscope/src/backend/services/validation.py index e045aa9..605f969 100644 --- a/periscope/src/backend/services/validation.py +++ b/periscope/src/backend/services/validation.py @@ -28,6 +28,7 @@ from backend.periscopex.led_current_check import check_led_current from backend.periscopex.lifecycle import check_lifecycle, load_lifecycle_dir from backend.periscopex.models import ComponentType, DesignGraph, Finding, ValidationReport from backend.periscopex.nc_pin_check import check_nc_pins +from backend.periscopex.parsers import ic_mpn_skip_reason from backend.periscopex.passive_rail_check import ( check_i2c_pullups, check_reset_pullups, @@ -150,10 +151,18 @@ async def validate_design_async( if comp.component_type != ComponentType.IC: continue mpn = (comp.mpn or "").strip() or (comp.value or "").strip() - if not mpn: - not_reviewed.append({"designator": ref, "reason": "no MPN in BOM"}) + skip = ic_mpn_skip_reason( + ref, + mpn=comp.mpn, + value=comp.value, + footprint=comp.footprint, + in_bom=ref in (graph.bom_fields or {}), + bom_value=(graph.bom_fields or {}).get(ref, {}).get("value"), + ) + if skip: + not_reviewed.append({"designator": ref, "reason": skip}) if on_progress: - await on_progress(ref, 0, "skipped", "no MPN in BOM") + await on_progress(ref, 0, "skipped", skip) continue pdf = _find_pdf(mpn, pdf_dir_path, storage=storage) if pdf: diff --git a/periscope/src/frontend/content/changelog.md b/periscope/src/frontend/content/changelog.md index 46b694a..c3ea31d 100644 --- a/periscope/src/frontend/content/changelog.md +++ b/periscope/src/frontend/content/changelog.md @@ -2,6 +2,13 @@ What's new in Periscope. +## 2.60.4 — 2026-09-21 — KiCad PNM and honest missing-MPN skips + +KiCad BOM column `PNM` is an MPN alias. Grouped `Reference` lists still split. IC `Value` fills MPN when PNM is blank (PCA9534ARGTR). Netlist-only U* with no BOM row are `INSUFFICIENT_EVIDENCE` (quoted Value/Footprint), not a silent drop and not an invented part number. + +- [Fixed] Progress skip text no longer says “no MPN in BOM” when the designator is absent from the CSV. +- [New] HubAudio KiCad BOM fixture: PNM header, U2/U3 Value fallback, L4 bead PNM, U13…U39 not invented. + ## 2.60.3 — 2026-09-21 — Ferrite Z from library MPN datasheet graph_build looks up the bead MPN PDF in project `uploads/datasheets` and `library/datasheets` (named files and blob refs). Z is filled only when the PDF states impedance (e.g. 120 Ω @ 100 MHz). Missing Z stays omitted; graph_build does not abort. diff --git a/tests/fixtures/hubaudio_kicad_bom.csv b/tests/fixtures/hubaudio_kicad_bom.csv new file mode 100644 index 0000000..a5c017f --- /dev/null +++ b/tests/fixtures/hubaudio_kicad_bom.csv @@ -0,0 +1,75 @@ +"Reference","Qty","Value","DNP","Exclude from BOM","Exclude from Board","Footprint","Datasheet","Description","PNM" +"BOOT1,RESET1","2","TS3425UA-3x4x2.5-160","","","","MIKILAB_TS3425UA_3x4x2_5_160:TS3425UA-3x4x2.5-160","","SMD tactile switch, 4-pin, 1-2 common and 3-4 common, 160gf","TS3425UA-3x4x2.5-160" +"C21,C22,C33,C47,C48,C49,C59,C60,C61,C65,C68,C70,C72,C73,C74,C75,C76,C77,C78,C79,C80,C86,C88,C119,C129,C134,C135,C162,C163,C172,C173,C174,C191,C192,C193,C194,C202","37","100nF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM21BR71H104KA01L","GRM21BR71H104KA01L" +"C23,C118","2","47uF","","","","Capacitor_SMD:C_1206_3216Metric","","CL32A476KOJNNNE","CL32A476KOJNNNE" +"C24,C38,C44","3","1uF","","","","Capacitor_SMD:C_1206_3216Metric","","12063C105KAT2A","12063C105KAT2A" +"C25,C26,C34,C36,C39,C40,C41,C45,C51,C52,C55,C56,C58,C121,C122,C158,C175,C176,C178,C179","20","10uF","","","","Capacitor_SMD:C_1206_3216Metric","","GRM31CR71E106KA12L","GRM31CR71E106KA12L" +"C27,C43,C53,C57,C63,C67,C105,C108,C111,C114,C117,C123,C165,C166,C167,C168,C169,C170,C171,C180,C182,C183,C190","23","1uF","","","","Capacitor_SMD:C_0805_2012Metric","","GCM21BR71E105KA56L","GCM21BR71E105KA56L" +"C29,C30","2","22pF","","","","Capacitor_SMD:C_0805_2012Metric","","CL21C220JBANNNC","CL21C220JBANNNC" +"C31,C32,C84,C85,C136,C164","6","10uF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM21BR61E106KA73L","GRM21BR61E106KA73L" +"C37,C42","2","1nF","","","","Capacitor_SMD:C_0805_2012Metric","","CC0805KRX7R9BB102","CC0805KRX7R9BB102" +"C62","1","470pF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM2165C1H471JA01D","GRM2165C1H471JA01D" +"C64,C189,C201,C203,C210,C211","6","22uF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM21BZ71A226ME15L","GRM21BZ71A226ME15L" +"C81","1","150pF","","","","Capacitor_SMD:C_0805_2012Metric","","08055A151FAT2A","08055A151FAT2A" +"C82","1","5.6nF","","","","Capacitor_SMD:C_0805_2012Metric","","GCM2195C1K562FA16D","GCM2195C1K562FA16D" +"C100","1","33pF","","","","Capacitor_SMD:C_0603_1608Metric","","GQM1875C2E330JB12D","GQM1875C2E330JB12D" +"C101","1","2.7pF","","","","Capacitor_SMD:C_0603_1608Metric","","GQM1875C2E2R7BB12D","GQM1875C2E2R7BB12D" +"C103,C106,C109,C112","4","8.2pF","","","","Capacitor_SMD:C_0805_2012Metric","","GQM2195C2E8R2CB12D","GQM2195C2E8R2CB12D" +"C104,C107,C110,C113","4","2.2nF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM2165C1H222JA01D","GRM2165C1H222JA01D" +"C115,C116","2","15pF","","","","Capacitor_SMD:C_0805_2012Metric","","GQM2195C2E150JB12D","GQM2195C2E150JB12D" +"C132,C133","2","20pF","","","","Capacitor_SMD:C_0805_2012Metric","","GRM21A5C2J200JA01","GRM21A5C2J200JA01" +"C195,C196,C197,C198,C199,C200,C204,C205,C206,C207,C208,C209","12","2.2uF","","","","Capacitor_SMD:C_0805_2012Metric","","CL21B225KPFNNNE","CL21B225KPFNNNE" +"D1,D4","2","D_TVS","","","","Diode_SMD:D_SOD-882","","PESD5V0X1BL","" +"D2","1","ESD9B5.0ST5G","","","","Diode_SMD:D_SOD-923","https://www.onsemi.com/pub/Collateral/ESD9B-D.PDF","ESD9B5.0ST5G","ESD9B5.0ST5G" +"D3","1","LED","","","","LED_SMD:LED_0805_2012Metric","","LTST-C170TBKT","LTST-C170TBKT" +"FB1","1","BLM18BB600SN1","","","","Inductor_SMD:L_0603_1608Metric","","BLM18BB600SN1","BLM18BB600SN1" +"J1","1","RJ45_Abracon_ARJP11A-MASA-B-A-EMU2","","","","Connector_RJ:RJ45_Abracon_ARJP11A-MA_Horizontal","https://abracon.com/Magnetics/lan/ARJP11A.PDF","RJ45 PoE 10/100 Base-TX Jack with Magnetic Module","RJ45_Abracon_ARJP11A-MASA-B-A-EMU2" +"J2","1","USB_C_Receptacle_USB2.0_16P","","","","Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12","https://www.usb.org/sites/default/files/documents/usb_type-c.zip","TYPE-C-31-M-12","TYPE-C-31-M-12" +"J3","1","AFBR-1624Z","","","","OptoDevice:Broadcom_AFBR-16xxZ_Horizontal","https://docs.broadcom.com/docs/AV02-4369EN","AFBR-1624Z","AFBR-1624Z" +"J4","1","AFBR-2529Z","","","","OptoDevice:Broadcom_AFBR-16xxZ_Horizontal","https://docs.broadcom.com/doc/AV02-4369EN","AFBR-2529Z","AFBR-2529Z" +"J5","1","RF/COAXIAL SMA BHD JACK STR 50OHM SOLDER","","","","Connector_Coaxial:SMA_Amphenol_132289_EdgeMount","","RF/COAXIAL SMA BHD JACK STR 50OHM SOLDER","RF/COAXIAL SMA BHD JACK STR 50OHM SOLDER" +"J6,J8,J9","3","SJ-3506-SMT-TR","","","","MIKILAB_SJ_3506_SMT_TR:CUI_SJ-3506-SMT-TR","","SJ-3506-SMT-TR","SJ-3506-SMT-TR" +"J_BAT2,J_POWER1","2","Conn_01x02","","","","Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Horizontal","","Conn_01x02","Conn_01x02" +"L4,L6,L8,L18,L21,L23,L24,L26","8","120 ohm","","","","Inductor_SMD:L_0805_2012Metric","","BLM21PG121SN1D","BLM21PG121SN1D" +"L5,L10","2","47uH","","","","Inductor_SMD:L_0603_1608Metric","","LSQNB160808T470M","LSQNB160808T470M" +"L12,L19,L25","3","1uH","","","","Inductor_SMD:L_0806_2016Metric","","WPN201610M1R0MT","WPN201610M1R0MT" +"L13","1","18nH","","","","Inductor_SMD:L_0603_1608Metric","","LQW18AN18NJ00D","LQW18AN18NJ00D" +"L14","1","120nH","","","","Inductor_SMD:L_0603_1608Metric","","LQW18ANR12J00D","LQW18ANR12J00D" +"L15","1","22nH","","","","Inductor_SMD:L_0603_1608Metric","","LQW18AN22NH00D","LQW18AN22NH00D" +"R1","1","510","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-07510RL","RC0805FR-07510RL" +"R2,R30","2","330","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-07330RL","RC0805FR-07330RL" +"R15,R16","2","100m","","","","Resistor_SMD:R_1206_3216Metric","","WSL1206R1000FEA","WSL1206R1000FEA" +"R18,R19","2","1k","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-071KL","RC0805FR-071KL" +"R34","1","12.1K","","","","Resistor_SMD:R_0805_2012Metric","","FRC0805F1212TS","FRC0805F1212TS" +"R35,R36,R44,R45,R52","5","4.7K","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-074K7L","RC0805FR-074K7L" +"R41,R42","2","5.1K","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-075K1L","RC0805FR-075K1L" +"R43,R47,R48,R58,R59,R72,R75,R76","8","10K","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-0710KL","RC0805FR-0710KL" +"R46","1","4.3K","","","","Resistor_SMD:R_0805_2012Metric","","0805W8F4301T5E","0805W8F4301T5E" +"R51","1","100","","","","Resistor_SMD:R_0603_1608Metric","","RC0603FR-07100RL","RC0603FR-07100RL" +"R73,R74","2","2.2K","","","","Resistor_SMD:R_0805_2012Metric","","0805W8F2201T5E","0805W8F2201T5E" +"R95,R96,R97,R98,R100","5","33","","","","Resistor_SMD:R_0805_2012Metric","","RC0805FR-0733RL — 33 Ω — 1% — 0805","RC0805FR-0733RL — 33 Ω — 1% — 0805" +"RN10,RN14,RN15,RN16,RN18","5","4x10k","","","","Resistor_SMD:R_Array_Convex_4x0402","","YC124-JR-0710KL","" +"RN11","1","49.9","","","","Resistor_SMD:R_Array_Convex_4x1206","","CAT16-49R9F4LF","" +"RN12","1","33","","","","Resistor_SMD:R_Array_Convex_2x0402","","2D02WGJ0330TCE","" +"RN13","1","10K","","","","Resistor_SMD:R_Array_Convex_4x0402","","YC124-FR-0710KL","" +"U1","1","TPS22965DSGR","","","","MIKILAB_TPS22965DSGR:SON50P200X200X80-9N","","TPS22965DSGR","TPS22965DSGR" +"U2,U3","2","PCA9534ARGTR","","","","Package_DFN_QFN:VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm","http://www.ti.com/lit/ds/symlink/tca9534.pdf","8-bit I/O Expander, I2C / SMBus interface, interrupt output, fixed address bits 0b0100xxx, TSSOP-16 / SOIC-16","" +"U4,U8","2","INA228","","","","Package_SO:TSSOP-10_3x3mm_P0.5mm","https://www.ti.com/lit/ds/symlink/ina228.pdf","INA228AQDGSRQ1","INA228AQDGSRQ1" +"U5","1","XTAC5212IRGER","","","","MIKILAB_XTAC5212IRGER:VQFN24_RGE_CORNERPADS_TEX","https://www.ti.com/lit/gpn/tac5212","XTAC5212IRGER","XTAC5212IRGER" +"U9,U14,U26","3","TPD2E007DCKR","","","","MIKILAB_TPD2E007DCKR:SOT65P210X110-3N","","TPD2E007DCKR","TPD2E007DCKR" +"U10","1","W25Q128JVS","","","","Package_SO:SOIC-8_5.3x5.3mm_P1.27mm","http://www.winbond.com/resource-files/w25q128jv_dtr%20revc%2003272018%20plus.pdf","W25Q128JVS","W25Q128JVS" +"U11","1","ESP32-S31-WROOM-3","","","","MIKILAB_ESP32_S31_WROOM_3:ESP32-S31-WROOM-3","https://documentation.espressif.com/esp32-s31-wroom-3_datasheet_en.html#pin_layout","ESP32-S31-WROOM-3","ESP32-S31-WROOM-3" +"U12","1","Si4684-A10-GM","","","","Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm_ThermalVias","https://www.skyworksinc.com/-/media/SkyWorks/SL/documents/public/product-summaries/Si4684-A10_PS.pdf","Si4684-A10-GM","Si4684-A10-GM" +"U16","1","AXP2101","","","","Package_DFN_QFN:QFN-40-1EP_5x5mm_P0.4mm_EP3.6x3.6mm_ThermalVias","https://www.lcsc.com/datasheet/C3036461.pdf","AXP2101","AXP2101" +"U17","1","24AA025E64","","","","Package_SO:SOIC-8_3.9x4.9mm_P1.27mm","https://ww1.microchip.com/downloads/aemDocuments/documents/MPD/ProductDocuments/DataSheets/2-Kbit-I2C-Serial-EEPROMs-+EUI-48-or-EUI-64-Node-20002124.pdf","24AA025E64","24AA025E64" +"U18","1","ADAU1467WBCPZ300R","","","","MIKILAB_ADAU1467WBCPZ300RL:ADAU1467WBCPZ300","https://www.analog.com/media/en/technical-documentation/data-sheets/adau1463-1467.pdf","ADAU1467WBCPZ300R","ADAU1467WBCPZ300R" +"U19","1","LAN8720A","","","","Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias","http://ww1.microchip.com/downloads/en/DeviceDoc/8720a.pdf","LAN8720A","LAN8720A" +"U24","1","25AA1024-I_SM","","","","Package_SO:SOIC-8_5.3x5.3mm_P1.27mm","","25AA1024-I_SM","25AA1024-I_SM" +"U27","1","W25Q16JW","","","","Package_SO:SOIC-8_5.3x5.3mm_P1.27mm","https://www.winbond.com/hq/support/documentation/levelOne.jsp?__locale=en&DocNo=DA00-W25Q16JV.1","W25Q16JW","W25Q16JW" +"U31,U32,U33,U37","4","TMP117NAIDRVR","","","","Package_SON:WSON-6-1EP_2x2mm_P0.65mm_EP1x1.6mm","https://www.ti.com/lit/ds/symlink/tmp117.pdf","TMP117NAIDRVR","TMP117NAIDRVR" +"U38","1","FSC-BT1058","","","","MIKILAB_FSC_BT1058:FSC-BT1058","https://document.feasycom.com/docs/datasheet/BT1058/FSC-BT1058_Datasheet_EN.pdf","FSC-BT1058","FSC-BT1058" +"U40","1","SC16IS740IPW,128","","","","Package_SO:TSSOP-16_4.4x5mm_P0.65mm","https://www.nxp.com/docs/en/data-sheet/SC16IS740_750_760.pdf","SC16IS740IPW,128","SC16IS740IPW,128" +"X1","1","ASE-50.000MHZ-LC-T","","","","Oscillator:Oscillator_SMD_Abracon_ASE-4Pin_3.2x2.5mm","http://www.abracon.com/Oscillators/ASV.pdf","3.3V CMOS SMD Crystal Clock Oscillator, Abracon","ASE-50.000MHZ-LC-T" +"Y1","1","ABM3B-12.288MHZ-10-1-U-T","","","","Crystal:Crystal_SMD_5032-4Pin_5.0x3.2mm","","Four pin crystal, GND on pins 2 and 4","ABM3B-12.288MHZ-10-1-U-T" +"Y2","1","ABM8-19.200MHZ-10-1-U-T","","","","Crystal:Crystal_SMD_Abracon_ABM8AIG-4Pin_3.2x2.5mm","","Four pin crystal, GND on pins 2 and 4","ABM8-19.200MHZ-10-1-U-T" +"Y4","1","12,288 MHz","","","","Crystal:Crystal_SMD_2520-4Pin_2.5x2.0mm","","FH1220004","FH1220004" diff --git a/tests/test_hubaudio_bom.py b/tests/test_hubaudio_bom.py new file mode 100644 index 0000000..6e32e6c --- /dev/null +++ b/tests/test_hubaudio_bom.py @@ -0,0 +1,47 @@ +"""HubAudio KiCad BOM: PNM column, grouped refs, no invented U13…U39 MPNs.""" + +from pathlib import Path + +from backend.periscopex.parsers import ic_mpn_skip_reason, parse_bom + +FIXTURE = Path(__file__).parent / "fixtures" / "hubaudio_kicad_bom.csv" +SKIPPED = ("U13", "U15", "U25", "U28", "U34", "U35", "U36", "U39") + + +def test_hubaudio_header_is_pnm_not_mpn(): + header = FIXTURE.read_text(encoding="utf-8-sig").splitlines()[0] + assert "PNM" in header + assert "Manufacturer Part Number" not in header + + +def test_hubaudio_pnm_and_value_fallback(): + bom = parse_bom(FIXTURE, mpn_col="Manufacturer Part Number") + assert bom["U1"]["mpn"] == "TPS22965DSGR" + assert bom["U10"]["mpn"] == "W25Q128JVS" + assert bom["U11"]["mpn"] == "ESP32-S31-WROOM-3" + # PNM blank, Value is the orderable MPN; grouped Reference list. + assert bom["U2"]["mpn"] == "PCA9534ARGTR" + assert bom["U3"]["mpn"] == "PCA9534ARGTR" + assert bom["U2"]["value"] == "PCA9534ARGTR" + assert (bom["U4"]["mpn"] == "INA228AQDGSRQ1") and bom["U8"]["mpn"] == "INA228AQDGSRQ1" + assert bom["L4"]["mpn"] == "BLM21PG121SN1D" + assert bom["L4"]["value"] == "120 ohm" + + +def test_hubaudio_skipped_refs_absent_no_invented_mpn(): + bom = parse_bom(FIXTURE, mpn_col="Manufacturer Part Number") + for ref in SKIPPED: + assert ref not in bom + reason = ic_mpn_skip_reason( + ref, mpn=None, value="", footprint="MIKILAB_TPD2E007DCKR:SOT65P210X110-3N", + in_bom=False, + ) + assert reason and reason.startswith("INSUFFICIENT_EVIDENCE") + assert "not in BOM" in reason + + +def test_ic_skip_empty_pnm_quotes_value(): + reason = ic_mpn_skip_reason( + "U99", mpn=None, value="", footprint="QFN", in_bom=True, bom_value="", + ) + assert reason and "PNM/MPN empty" in reason diff --git a/tests/test_validation_deterministic_seed.py b/tests/test_validation_deterministic_seed.py index 2d6fbdb..bef5014 100644 --- a/tests/test_validation_deterministic_seed.py +++ b/tests/test_validation_deterministic_seed.py @@ -77,3 +77,7 @@ async def test_deterministic_findings_seeded_and_not_reviewed(tmp_path): assert data["summary"]["ERROR"] >= 1 assert {x["designator"] for x in data["not_reviewed"]} == {"U3", "U6"} + by_ref = {x["designator"]: x["reason"] for x in data["not_reviewed"]} + assert by_ref["U3"] == "no datasheet PDF" + assert by_ref["U6"].startswith("INSUFFICIENT_EVIDENCE") + assert "not in BOM" in by_ref["U6"]