app
Adom Chip Fetcher
Public Made by Adomby adom
Your whole parts library — manufacturer-grade chip CAD (symbol, footprint, 3D) one tap from your EDA tool.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
use serde::Serialize;
use std::path::Path;
#[derive(Serialize)]
pub struct Chip {
pub mpn: String,
pub vendor: String,
pub package: String,
pub family: String,
pub fully_bundled: bool,
pub has_step: bool,
pub has_sym: bool, // KiCad .kicad_sym
pub has_mod: bool, // KiCad .kicad_mod
pub has_altium_sch: bool, // Altium .SchLib
pub has_altium_pcb: bool, // Altium .PcbLib
pub has_altium_intlib: bool, // Altium .IntLib (compiled — SchLib+PcbLib in one)
pub has_altium: bool, // (sch && pcb) || intlib — derived for the UI
pub altium_not_available: bool, // info.json says "not_available" (exhausted all sources)
pub has_fusion_lbr: bool, // Fusion 360 .lbr
pub fusion_not_available: bool, // info.json says "not_available" (exhausted all sources)
pub has_pdf: bool,
pub pdf_count: usize,
pub has_info: bool,
pub has_thumb_symbol: bool,
pub sym_rev: u64, // mtime(ms) of <MPN>-symbol.svg — bumps when adom-symbol re-renders, so the dashboard reloads the thumbnail instantly
pub has_thumb_footprint: bool,
pub has_thumb_chip3d: bool, // Z-up iso (canonical, <MPN>-3d-iso.png)
pub has_thumb_chip3d_yup: bool, // Y-up iso (<MPN>-3d-iso-yup.png) — ME / Y-up source convention
/// chip-thumbnailer's auto-inferred source-up axis: "z" or "y" (or "?"
/// when the manifest is missing). Read from <MPN>-thumbs.json's
/// thumbnails.threeD.inferredSourceUpAxis.
pub inferred_up_axis: String,
/// The orientation the user has chosen for this chip ("z" / "y"). Falls
/// back to the inferred axis when the user hasn't overridden. Stored in
/// info.json's chosen_up_axis field so downstream tools (adom-step,
/// adom-chiplinter, etc.) can pick up the right orientation.
pub chosen_up_axis: String,
pub step_source: String,
pub size_bytes: u64,
pub files: Vec<String>,
pub info: serde_json::Value,
pub stock: serde_json::Value,
/// ds2sf v0.5.1+ outputs: pixel-trustworthy footprint SVG that
/// downstream consumers (chipsmith visual diff, sym_create previews,
/// wiki page hero) use as ground truth. Path is relative to the
/// chip directory; source slug is "service-kicad-stdlib" (canonical
/// KiCad render) or "ds2sf-synthesized" (best-effort from extracted
/// dims; layout reference only). Read from
/// `<MPN>-extraction.result.json`'s `outputs.footprint_svg` field.
pub authoritative_footprint_svg: Option<String>,
pub authoritative_footprint_svg_source: Option<String>,
/// concur v0.x+ persisted state: golden / golden_with_variants /
/// divergent / unrecoverable. Read from `<MPN>-validation-status.json`
/// (which our validate.rs writes after every concur run). Surfaces on
/// the dashboard so the user sees the gold-dataset state per card.
pub validation_status: Option<String>,
pub validation_variants: Vec<String>,
/// Pin-1 sign-off from chipsmith (`<MPN>.chipsmith.json`). `pin1_signed` = the
/// user lined up the 3D chip's pin-1 to the footprint's pin-1 in chipsmith and
/// signed off; `pin1_corner` is which corner pin-1 sits in (NW/NE/SW/SE). The
/// baked `<MPN>.chipsmith.step` is the orientation-corrected canonical model
/// downstream tools consume. None/false until signed.
pub pin1_signed: bool,
pub pin1_corner: Option<String>,
pub pin1_signed_at: Option<String>,
pub pin1_measure_ok: Option<bool>,
/// Which corner the FOOTPRINT's pin-1 sits in (NW/NE/SW/SE), derived from the
/// `.kicad_mod` (pad "1" vs the pad centroid). This is the TARGET the 3D chip's
/// pin-1 must line up with — shown as a hint on unsigned cards, and compared
/// against `pin1_corner` to flag a chip-vs-footprint mismatch once signed.
pub fp_pin1_corner: Option<String>,
/// Per-file provenance: which ladder source + URL each library file
/// came from. Chipsmith reads this for tooltips. Written by
/// import::import_path_with_provenance at import time.
pub file_provenance: serde_json::Value,
/// Provenance for the symbol/footprint thumbnail carat pulldowns: which
/// method(s) produced an available variant (vendor source + ds2sf).
pub symbol_sources: Vec<SourceTag>,
pub footprint_sources: Vec<SourceTag>,
/// chip-thumbnailer's per-kind failure reasons, read from
/// `.thumb-errors.json` (e.g. {"sym":"...","fp":"...","3d":"..."}).
/// Lets the dashboard render a "render failed" tile state with the
/// reason, instead of a blank tile that's indistinguishable from
/// "source file absent". Null when no errors file is present.
pub thumb_errors: serde_json::Value,
}
#[derive(Serialize)]
pub struct Library {
pub chips: Vec<Chip>,
pub total_size: u64,
pub fully_count: usize,
pub stub_count: usize,
}
/// Which corner the footprint's pin-1 sits in (NW/NE/SW/SE), from the `.kicad_mod`.
/// We take pad "1"'s position vs the centroid of all pads. KiCad footprint space
/// is **Y-down** (negative Y = top/north). Returns None when there's no pad "1"
/// or pin-1 sits dead-centre (ambiguous). Pure string parse — no s-expr dep.
fn footprint_pin1_corner(dir: &Path, mpn: &str) -> Option<String> {
let content = std::fs::read_to_string(dir.join(format!("{mpn}.kicad_mod"))).ok()?;
let mut p1: Option<(f64, f64)> = None;
let (mut sx, mut sy, mut n) = (0.0f64, 0.0f64, 0usize);
for line in content.lines() {
let l = line.trim_start();
if !l.starts_with("(pad ") {
continue;
}
// pad name = text between the first pair of quotes: (pad "1" smd ...
let name = match l.split('"').nth(1) {
Some(s) => s,
None => continue,
};
// coords follow "(at ": (at X Y[ rot])
let at = match l.find("(at ") {
Some(i) => &l[i + 4..],
None => continue,
};
let mut it = at.split_whitespace();
let x = match it.next().and_then(|t| t.trim_end_matches(')').parse::<f64>().ok()) {
Some(v) => v,
None => continue,
};
let y = match it.next().and_then(|t| t.trim_end_matches(')').parse::<f64>().ok()) {
Some(v) => v,
None => continue,
};
sx += x;
sy += y;
n += 1;
if name == "1" {
p1 = Some((x, y));
}
}
if n == 0 {
return None;
}
let (x, y) = p1?;
let (cx, cy) = (sx / n as f64, sy / n as f64);
let ew = if x < cx - 0.01 { "W" } else if x > cx + 0.01 { "E" } else { "" };
let ns = if y < cy - 0.01 { "N" } else if y > cy + 0.01 { "S" } else { "" }; // Y-down
if ew.is_empty() || ns.is_empty() {
return None; // pin-1 centred on one axis → ambiguous, no hint
}
Some(format!("{ns}{ew}"))
}
fn vendor_from_mpn(mpn: &str) -> &'static str {
let m = mpn.to_uppercase();
let prefixes = [
("TPS65987", "TI"), ("MCXN", "NXP"), ("ESP32", "Espressif"),
("ICE40", "Lattice"), ("R7FA", "Renesas"), ("DA14", "Renesas"),
("ATSAM", "Microchip"), ("ATME", "Microchip"), ("ATTI", "Microchip"),
("VL5", "ST"), ("STM", "ST"), ("LSM", "ST"),
("BME", "Bosch"), ("BMI", "Bosch"), ("BMP", "Bosch"),
("PCA9", "NXP"), ("LPC", "NXP"),
("AD7", "ADI"), ("AD8", "ADI"), ("LTC", "ADI"), ("MAX", "ADI"),
("DMG", "Diodes"), ("DMP", "Diodes"), ("DMN", "Diodes"),
("WAGO", "WAGO"), ("NRF", "Nordic"),
("TPS", "TI"), ("ADS", "TI"), ("INA", "TI"), ("LM", "TI"),
("DRV", "TI"), ("MCF", "TI"), ("BQ", "TI"), ("MSP", "TI"),
("VS", "Vishay"), ("SI", "Vishay"), ("CRCW", "Vishay"),
];
for (p, v) in prefixes.iter() {
if m.starts_with(p) { return v; }
}
"?"
}
pub fn scan() -> anyhow::Result<Library> {
let root = crate::library::root();
let mut chips = Vec::new();
let mut total_size = 0u64;
if !root.exists() {
return Ok(Library { chips, total_size: 0, fully_count: 0, stub_count: 0 });
}
for entry in std::fs::read_dir(&root)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() { continue; }
let mpn = entry.file_name().to_string_lossy().to_string();
// ENFORCE provenance at the source: any file that landed without going
// through import gets its origin recorded now (derived from info.json, or
// flagged "unknown"). Idempotent — only writes files with no entry yet —
// so a stray un-sourced file can never silently persist past one scan.
let _ = crate::import::backfill_dir(&path, &mpn);
let chip = read_chip(&path, &mpn)?;
total_size += chip.size_bytes;
chips.push(chip);
}
chips.sort_by(|a, b| a.mpn.cmp(&b.mpn));
let fully_count = chips.iter().filter(|c| c.fully_bundled).count();
let stub_count = chips.len() - fully_count;
Ok(Library { chips, total_size, fully_count, stub_count })
}
fn read_chip(dir: &Path, mpn: &str) -> anyhow::Result<Chip> {
let mut files = Vec::new();
let mut size = 0u64;
let mut pdf_count = 0;
let mut has_pdf = false;
let mut has_step = false;
let mut has_sym = false;
let mut has_mod = false;
let mut has_altium_sch = false;
let mut has_altium_pcb = false;
let mut has_altium_intlib = false;
let mut has_fusion_lbr = false;
let mut has_info = false;
let mut info_value = serde_json::Value::Null;
let mut stock_value = serde_json::Value::Null;
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if !entry.path().is_file() { continue; }
let meta = entry.metadata()?;
size += meta.len();
files.push(name.clone());
if name == format!("{mpn}.step") || name == format!("{mpn}.stp") { has_step = true; }
if name == format!("{mpn}.kicad_sym") { has_sym = true; }
if name == format!("{mpn}.kicad_mod") { has_mod = true; }
if name == format!("{mpn}.SchLib") { has_altium_sch = true; }
if name == format!("{mpn}.PcbLib") { has_altium_pcb = true; }
if name == format!("{mpn}.IntLib") { has_altium_intlib = true; }
if name == format!("{mpn}.lbr") { has_fusion_lbr = true; }
if name == format!("{mpn}.pdf") || name == format!("{mpn}-datasheet.pdf") { has_pdf = true; }
if name.ends_with(".pdf") { pdf_count += 1; }
if name == "info.json" {
has_info = true;
if let Ok(s) = std::fs::read_to_string(entry.path()) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
info_value = v;
}
}
}
if name == "stock.json" {
if let Ok(s) = std::fs::read_to_string(entry.path()) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
stock_value = v;
}
}
}
}
files.sort();
let vendor = info_value.get("manufacturer").and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| vendor_from_mpn(mpn).to_string());
let package = info_value.get("package").and_then(|v| v.as_str())
.unwrap_or("?").to_string();
let family = info_value.get("family").and_then(|v| v.as_str())
.unwrap_or("").to_string();
let step_source = info_value.get("step_source").and_then(|v| v.as_str())
.unwrap_or("—").to_string();
// Thumbnails live alongside the source files using chip-thumbnailer's
// canonical naming: <MPN>-symbol.svg / <MPN>-footprint.svg / <MPN>-3d-iso.png
let has_thumb_symbol = dir.join(format!("{mpn}-symbol.svg")).is_file();
let sym_rev = std::fs::metadata(dir.join(format!("{mpn}-symbol.svg"))).ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as u64).unwrap_or(0);
let has_thumb_footprint = dir.join(format!("{mpn}-footprint.svg")).is_file();
let has_thumb_chip3d = dir.join(format!("{mpn}-3d-iso.png")).is_file();
let has_thumb_chip3d_yup = dir.join(format!("{mpn}-3d-iso-yup.png")).is_file();
// Read chip-thumbnailer's manifest for the auto-inferred up axis. Defaults
// to "z" (STEP canonical) when the manifest is absent.
let inferred_up_axis = std::fs::read_to_string(dir.join(format!("{mpn}-thumbs.json")))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.and_then(|v| v.get("thumbnails")
.and_then(|t| t.get("threeD"))
.and_then(|td| td.get("inferredSourceUpAxis"))
.and_then(|a| a.as_str())
.map(|s| s.to_string()))
.unwrap_or_else(|| "z".to_string());
// User's chosen axis (if they overrode). Falls back to inferred.
let chosen_up_axis = info_value.get("chosen_up_axis")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| inferred_up_axis.clone());
// Altium — accept either the SchLib+PcbLib pair OR a compiled .IntLib (which
// contains both internally; it's what Component Search Engine downloads ship).
let has_altium = (has_altium_sch && has_altium_pcb) || has_altium_intlib;
let altium_not_available = info_value.get("altium_source")
.and_then(|v| v.as_str()) == Some("not_available");
let fusion_not_available = info_value.get("fusion_source")
.and_then(|v| v.as_str()) == Some("not_available");
// "Fully bundled" = all 3 EDA tools (KiCad sym+mod, Altium, Fusion lbr) + STEP.
// Datasheet PDF is intentionally not required — many chips ship with manufacturer
// datasheets via separate fetch and shouldn't gate the bundled-ness signal.
let fully_bundled = has_step
&& has_sym && has_mod
&& has_altium
&& has_fusion_lbr;
// ds2sf v0.5.1+ outputs: pull authoritative footprint SVG path + source
// from <MPN>-extraction.result.json's `outputs` block. Stable contract
// fields per the v0.5.1 release. Also relativize to chip dir so the
// dashboard can serve them directly.
let ext_result = std::fs::read_to_string(dir.join(format!("{mpn}-extraction.result.json")))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
let authoritative_footprint_svg = ext_result.as_ref()
.and_then(|v| v.get("outputs"))
.and_then(|o| o.get("footprint_svg"))
.and_then(|s| s.as_str())
.map(|p| {
// Strip the absolute prefix so the dashboard can serve it.
std::path::Path::new(p)
.file_name()
.and_then(|n| n.to_str())
.map(String::from)
.unwrap_or_else(|| p.to_string())
});
let authoritative_footprint_svg_source = ext_result.as_ref()
.and_then(|v| v.get("outputs"))
.and_then(|o| o.get("footprint_svg_source"))
.and_then(|s| s.as_str())
.map(String::from);
// concur validation status: read from <MPN>-validation-status.json
// (which validate.rs writes after every concur run). Surfaces on the
// dashboard so the user sees the gold-dataset state per card.
let validation = std::fs::read_to_string(dir.join(format!("{mpn}-validation-status.json")))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
let validation_status = validation.as_ref()
.and_then(|v| v.get("concur_status"))
.and_then(|s| s.as_str())
.map(String::from);
let validation_variants = validation.as_ref()
.and_then(|v| v.get("variants"))
.and_then(|a| a.as_array())
.map(|arr| arr.iter()
.filter_map(|e| e.get("kind").and_then(|k| k.as_str()).map(String::from))
.collect())
.unwrap_or_default();
// Pin-1 sign-off from chipsmith (<MPN>.chipsmith.json). The user lines up the
// 3D chip's pin-1 with the footprint's pin-1 in chipsmith + signs off; we read
// it back so pin-1 is visible on the card and the baked .chipsmith.step is the
// canonical placement model for downstream tools. (See the chipsmith launch.)
let chipsmith = std::fs::read_to_string(dir.join(format!("{mpn}.chipsmith.json")))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
let pin1_signed = chipsmith.as_ref().map(|v| v.get("signed_at").is_some()).unwrap_or(false);
let pin1_corner = chipsmith.as_ref()
.and_then(|v| v.get("user_placed_pin1")).and_then(|p| p.get("corner"))
.and_then(|c| c.as_str()).map(String::from);
let pin1_signed_at = chipsmith.as_ref()
.and_then(|v| v.get("signed_at")).and_then(|s| s.as_str()).map(String::from);
let pin1_measure_ok = chipsmith.as_ref()
.and_then(|v| v.get("measurements")).and_then(|m| m.get("passes")).and_then(|b| b.as_bool());
let fp_pin1_corner = footprint_pin1_corner(&dir, mpn);
// Per-file provenance map — chipsmith reads this for tooltips.
let file_provenance = std::fs::read_to_string(dir.join(format!("{mpn}-file-provenance.json")))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.unwrap_or(serde_json::Value::Null);
// chip-thumbnailer's per-kind failure reasons (.thumb-errors.json). Surfaced
// so a failed render shows as a distinct tile state, not a silent blank.
let thumb_errors = std::fs::read_to_string(dir.join(".thumb-errors.json"))
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.unwrap_or(serde_json::Value::Null);
// Per-kind source provenance for the symbol/footprint carat pulldowns: which
// method produced each available variant. The current (rendered) thumbnail
// is the canonical .kicad_sym/.kicad_mod; ds2sf is an alternate from the
// datasheet. The frontend renders these as a ▾ carat like the 3D one.
let symbol_sources = source_tags(
&dir,
has_sym,
&format!("{mpn}.kicad_sym"),
"kicad_sym",
&format!("{mpn}-symbol.extracted.json"),
&file_provenance,
&info_value,
);
let footprint_sources = source_tags(
&dir,
has_mod,
&format!("{mpn}.kicad_mod"),
"kicad_mod",
&format!("{mpn}-footprint.extracted.json"),
&file_provenance,
&info_value,
);
Ok(Chip {
mpn: mpn.to_string(),
vendor, package, family,
fully_bundled,
has_step, has_sym, has_mod,
has_altium_sch, has_altium_pcb, has_altium_intlib, has_altium, altium_not_available,
has_fusion_lbr, fusion_not_available,
has_pdf, pdf_count,
has_info,
has_thumb_symbol, sym_rev, has_thumb_footprint, has_thumb_chip3d, has_thumb_chip3d_yup,
inferred_up_axis, chosen_up_axis,
step_source,
size_bytes: size,
files,
info: info_value,
stock: stock_value,
authoritative_footprint_svg,
authoritative_footprint_svg_source,
validation_status,
validation_variants,
pin1_signed, pin1_corner, pin1_signed_at, pin1_measure_ok,
fp_pin1_corner,
file_provenance,
thumb_errors,
symbol_sources,
footprint_sources,
})
}
/// A provenance entry for a symbol/footprint thumbnail carat.
#[derive(serde::Serialize)]
pub struct SourceTag {
pub method: String, // "vendor" | "ds2sf" | "manufacturer" | source slug
pub label: String, // human label, e.g. "SnapEDA", "ds2sf · datasheet"
pub current: bool, // is this the variant currently rendered as the thumbnail?
pub thumb: bool, // does a thumbnail exist for this variant (show it in the carat)?
}
fn source_label(slug: &str) -> String {
match slug {
"snapmagic" | "snapeda" => "SnapEDA",
"ultralibrarian" | "cse_ul" | "ul" => "Ultra Librarian",
"cse" | "componentsearch" => "Component Search",
"manufacturer" | "mfr" => "Manufacturer",
"mouser" => "Mouser",
"digikey" => "DigiKey",
"arrow" => "Arrow",
"lcsc" => "LCSC",
"kicad_community" => "KiCad community",
"ds2sf" => "ds2sf · datasheet",
other => other,
}
.to_string()
}
/// Build the source list for a symbol/footprint: the canonical vendor file (with
/// its provenance origin) + the ds2sf datasheet extraction when present.
fn source_tags(
dir: &std::path::Path,
has_canonical: bool,
canonical_file: &str,
sor_key: &str,
ds2sf_file: &str,
provenance: &serde_json::Value,
info: &serde_json::Value,
) -> Vec<SourceTag> {
let mpn_prefix = canonical_file.trim_end_matches(&format!(".{sor_key}"));
let ext_suffix = format!(".{sor_key}");
let thumb_kind = if sor_key == "kicad_sym" { "symbol" } else { "footprint" };
let canon_thumb = dir.join(format!("{mpn_prefix}-{thumb_kind}.svg")).is_file();
// Label by WHERE it was downloaded (discovery_source), NOT who authored it
// (content_origin). Aggregators are never "the manufacturer"; manufacturer-
// direct names the site. See provenance::source_label.
let manufacturer = info.get("manufacturer").and_then(|m| m.as_str()).unwrap_or("");
let canon_disc = provenance
.get(canonical_file)
.and_then(|e| e.get("discovery_source").or_else(|| e.get("content_origin")))
.and_then(|s| s.as_str())
.or_else(|| info.get("source_of_record").and_then(|s| s.get(sor_key)).and_then(|s| s.as_str()))
// Don't FALSELY claim "Manufacturer (direct site)" when we never recorded
// a source — that misleading default is what made the carat look wrong
// ("Manufacturer direct site" on a chip we couldn't actually attribute).
.unwrap_or("unknown")
.to_string();
// chip-fetcher's goal: show EVERY library + format we pulled down, named, so
// the user can compare them and pick. One carat entry PER (source × format):
// "Component Search Engine - KiCad Library"
// "Component Search Engine - Altium Library"
// "Component Search Engine - Fusion 360 Library"
// "ds2sf Texas Instruments (ti.com)"
let src = crate::provenance::source_label(&canon_disc, manufacturer);
let has = |f: String| dir.join(f).is_file();
let mut tags = Vec::new();
// KiCad library — the canonical render shown on the tile (current source).
// When the source IS the KiCad standard library, don't say "KiCad library,
// from KiCad standard library" — the source already names the format.
if has_canonical {
let kicad_label = if src.starts_with("KiCad") { src.clone() } else { format!("KiCad library, from {src}") };
tags.push(SourceTag { method: "kicad".into(), label: kicad_label, current: true, thumb: canon_thumb });
}
// Altium library (.SchLib has the symbol/device, .PcbLib the footprint).
// adom-sfconvert turns it into KiCad sym+mod via KiCad 10 (service-kicad),
// rendered to its own variant thumbnail — so the carat previews the Altium
// library's OWN geometry.
let altium = if sor_key == "kicad_sym" { format!("{mpn_prefix}.SchLib") } else { format!("{mpn_prefix}.PcbLib") };
if has(altium) {
let altium_thumb = dir.join(format!("{mpn_prefix}-{thumb_kind}.altium.svg")).is_file();
tags.push(SourceTag { method: "altium".into(), label: format!("Altium library, from {src}"), current: false, thumb: altium_thumb });
}
// Fusion 360 / EAGLE library (.lbr holds both the device + the package).
// adom-sfconvert turns it into KiCad sym+mod, rendered to its own variant
// thumbnail — so the carat previews the Fusion library's OWN geometry.
if has(format!("{mpn_prefix}.lbr")) {
let fusion_thumb = dir.join(format!("{mpn_prefix}-{thumb_kind}.fusion.svg")).is_file();
tags.push(SourceTag { method: "fusion".into(), label: format!("Fusion 360 library, from {src}"), current: false, thumb: fusion_thumb });
}
// The manufacturer DATASHEET, extracted by ds2sf — the independent check.
let ds2sf_thumb = dir.join(format!("{mpn_prefix}-{thumb_kind}.ds2sf.svg")).is_file();
if dir.join(ds2sf_file).is_file() && ds2sf_thumb {
let mfr = crate::provenance::source_label("manufacturer", manufacturer);
tags.push(SourceTag { method: "ds2sf".into(), label: format!("ds2sf {mfr}"), current: !has_canonical, thumb: true });
}
tags
}