Reuse datasheet family aliases and skip the LLM for catalog passives.

Store BOM MPN and orderable code on the same PDF blob, derive TI datasheet slugs from package/Q1 codes, and map LCSC/DigiKey R/C/L parameters to specs without a model call.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 02:11:49 +02:00
co-authored by Cursor
parent 27a3ce8bd0
commit dd46ce1da1
11 changed files with 437 additions and 30 deletions
+52 -6
View File
@@ -56,11 +56,19 @@ class DatasheetHit:
error: str | None = None
url: str | None = None
source: str | None = None # "lcsc" | "ti" | "digikey" | ...
catalog_mpn: str | None = None # orderable code that actually matched
@property
def ok(self) -> bool:
return self.pdf_bytes is not None
@property
def alias_mpns(self) -> list[str]:
extra = (self.catalog_mpn or "").strip()
if extra and extra.upper() != (self.mpn or "").upper():
return [extra]
return []
def _alnum(mpn: str) -> str:
return re.sub(r"[^A-Z0-9]", "", mpn.upper())
@@ -242,8 +250,16 @@ async def _from_lcsc(mpn: str, lcsc_id: str | None) -> DatasheetHit | None:
except Exception as exc:
log.info("LCSC PDF download failed for %s (%s): %s", mpn, url, exc)
return DatasheetHit(mpn, error=f"LCSC download failed: {exc}", url=url, source="lcsc")
catalog = (
product.get("productModel")
or product.get("productName")
or ""
)
log.info("Fetched datasheet for %s via LCSC (%d KB)", mpn, len(pdf) // 1024)
return DatasheetHit(mpn, pdf_bytes=pdf, url=url, source="lcsc")
return DatasheetHit(
mpn, pdf_bytes=pdf, url=url, source="lcsc",
catalog_mpn=str(catalog) or None,
)
def _strip_packing_alnum(mpn: str) -> str | None:
@@ -255,17 +271,46 @@ def _strip_packing_alnum(mpn: str) -> str | None:
return None
_TI_PACKAGE_SUFFIXES = (
"dbvr", "dbvt", "dbv", "pwr", "pwt", "pw", "rger", "rget", "rge",
"dgsr", "dgsk", "dgs", "ydtr", "ydt", "dcnr", "dcnt", "dcn",
"rgtr", "rgtt", "rgt", "runr", "runt", "dckr", "dckt", "dck",
)
def _ti_slugs(mpn: str) -> list[str]:
"""Candidate TI datasheet slugs, most specific first."""
raw = mpn.lower().replace("/", "-").strip("-")
slugs = [raw]
# Longest packing / orderable suffixes first so "sptr" is not clipped to "sp".
slugs: list[str] = []
def add(value: str) -> None:
value = value.strip("-")
if value and value not in slugs:
slugs.append(value)
add(raw)
for suffix in ("-t/r", "/tr", "-tr", "-reel", "sptr", "ptr", "mtr", "tr"):
if raw.endswith(suffix) and len(raw) > len(suffix) + 3:
base = raw[: -len(suffix)].rstrip("-")
if base and base not in slugs:
slugs.append(base)
add(raw[: -len(suffix)].rstrip("-"))
break
for pkg in _TI_PACKAGE_SUFFIXES:
if raw.endswith(pkg) and len(raw) > len(pkg) + 4:
add(raw[: -len(pkg)])
break
# Orderable INA228AQDGSRQ1 → datasheet ina228-q1 / ina228
prefixes = sorted(_TI_PREFIXES, key=len, reverse=True)
for prefix in prefixes:
if not raw.startswith(prefix):
continue
rest = raw[len(prefix):]
m = re.match(r"(\d{2,})", rest)
if not m:
break
family = f"{prefix}{m.group(1)}"
add(family)
if "q1" in raw:
add(f"{family}-q1")
break
return slugs
@@ -303,6 +348,7 @@ async def _from_digikey(mpn: str) -> DatasheetHit | None:
if result.ok:
return DatasheetHit(
mpn, pdf_bytes=result.pdf_bytes, url=result.url, source="digikey",
catalog_mpn=getattr(result, "catalog_mpn", None),
)
return DatasheetHit(mpn, error=result.error, url=result.url, source="digikey")