diff --git a/backend/services/llm/local_skill.py b/backend/services/llm/local_skill.py index b439d06..338cb17 100644 --- a/backend/services/llm/local_skill.py +++ b/backend/services/llm/local_skill.py @@ -148,8 +148,12 @@ async def run_skill_locally( errors: list[str] = [] if validator is not None: + check = dict(payload) + mpn_hint = re.search(r"MPN:\s*(\S+)", user_text or "", re.I) + if mpn_hint and "mpn" not in check: + check["mpn"] = mpn_hint.group(1).rstrip(".,;") try: - errors = list(validator(payload) or []) + errors = list(validator(check) or []) except Exception as exc: log.warning( "Skill %s validate.py raised: %s", skill_name, exc, diff --git a/backend/skills_manifest.json b/backend/skills_manifest.json index 52469aa..8daa4b9 100644 --- a/backend/skills_manifest.json +++ b/backend/skills_manifest.json @@ -1,5 +1,5 @@ { - "default_model_version": "1.5.0", + "default_model_version": "1.6.0", "extract-pintable": { "skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY", "latest_version": "1784798970179642", diff --git a/skills/extract-pintable/SKILL.md b/skills/extract-pintable/SKILL.md index 969bed5..b924053 100644 --- a/skills/extract-pintable/SKILL.md +++ b/skills/extract-pintable/SKILL.md @@ -30,6 +30,9 @@ Rules for pin extraction: - For multiplexed pins, put the primary name in `name` and alternates in `functions` - If the datasheet has separate tables for different packages, extract for the package matching the MPN - Pay careful attention to pin numbering — off-by-one errors here break everything downstream +- **Modules vs bare die (critical).** MPNs containing `WROOM`, `WROVER`, `MODULE`, `MOD-`, or `SIP` are *modules*. Extract the **module landing-pad table** (connector pins the schematic uses). Do **not** extract the SoC/QFN ball map from a nested chip chapter or a sibling chip-only PDF. + - Espressif WROOM: pin 1 is GND (often a group of GND pads). Pin 1 named `ANT`, `CHIP_PU`, or `XTAL_*` means you grabbed the bare ESP32 die table — that will mark every module GND as “antenna shorted” and is invalid. + - Crystal, RF antenna, and flash on a WROOM module are **inside the can**; they must not appear as schematic pin numbers. ### 3. Extract package info diff --git a/skills/extract-pintable/validate.py b/skills/extract-pintable/validate.py index fd24d24..7d1f186 100644 --- a/skills/extract-pintable/validate.py +++ b/skills/extract-pintable/validate.py @@ -2,6 +2,7 @@ """Validate extraction output against the pintable schema.""" import json +import re import sys from pathlib import Path @@ -47,6 +48,24 @@ def validate(data: dict) -> list[str]: if dupes: errors.append(f"Duplicate pin numbers: {dupes}") + names = { + str(p.get("number")): str(p.get("name") or "").upper() + for p in pins if "number" in p + } + pin1 = names.get("1", "") + looks_like_rf_die = bool( + re.search(r"\bANT\b|^CHIP_PU$|^XTAL", pin1) + and any("XTAL" in n for n in names.values()) + ) + mpn = str(data.get("mpn") or "") + is_module_mpn = bool(re.search(r"WROOM|WROVER|\bMODULE\b|\bSIP\b", mpn, re.I)) + if looks_like_rf_die and is_module_mpn: + errors.append( + "Pin 1 looks like a bare RF SoC ball (ANT/CHIP_PU) with XTAL " + "pins in the table. Module footprints (WROOM) use pad 1 = GND; " + "extract the module landing-pad table, not the die map." + ) + if "absolute_maximum_ratings" in data: ratings = data["absolute_maximum_ratings"] if ratings is not None and not isinstance(ratings, list): diff --git a/tests/test_deepseek_provider.py b/tests/test_deepseek_provider.py index 831721b..a33e29c 100644 --- a/tests/test_deepseek_provider.py +++ b/tests/test_deepseek_provider.py @@ -212,6 +212,7 @@ def test_local_skills_load(): validate = load_skill_validator("extract-pintable") assert validate is not None errors = validate({ + "mpn": "MSPM0G3507SPTR", "component_subtype": "ic.mcu", "component_subtype_description": "MCU", "package_info": {"base_family": "MSPM0", "package": "LQFP-48", "pin_count": 2}, @@ -223,6 +224,23 @@ def test_local_skills_load(): assert errors == [] +def test_wroom_rejects_bare_soc_pin1_ant(): + from backend.services.llm.local_skill import load_skill_validator + validate = load_skill_validator("extract-pintable") + errors = validate({ + "mpn": "ESP32-S31-WROOM-3", + "component_subtype": "ic.mcu", + "package_info": {"base_family": "ESP32-S31", "package": "module", "pin_count": 2}, + "pintable": [ + {"number": 1, "name": "ANT"}, + {"number": 2, "name": "CHIP_PU"}, + {"number": 78, "name": "XTAL_N"}, + {"number": 79, "name": "XTAL_P"}, + ], + }) + assert any("module" in e.lower() or "pad" in e.lower() or "SoC" in e or "WROOM" in e for e in errors) + + def test_factory_routes_deepseek(monkeypatch): monkeypatch.setattr(settings, "deepseek_api_key", "sk-test") from backend.services.llm.factory import get_provider_by_name