app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
Publish 1.6.0
9 files changed
+352−19
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-lbr"-version = "1.5.0"+version = "1.6.0" dependencies = [ "anyhow", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "1.5.0"+version = "1.6.0" edition = "2021" description = "EAGLE .lbr generator, linter, and validator for Fusion 360 Electronics. Converts KiCad symbols + footprints to complete EAGLE libraries."
README.md+11@@ -43,6 +43,17 @@ Altium *native* `.SchLib/.PcbLib` step is left to the **Altium bridge** with a staged hand-off — adom-lbr never drives an app. Use `--targets kicad,fusion` to emit a subset. `generate`/`bind-3d` remain for single-format work. +**BOM attributes:** pass `--mfr --mpn --mouser --digikey --lcsc` (chip-fetcher+looks these up) and they're written into the library's device technology as+`MF`/`MPN`/`MOUSER`/`DIGIKEY`/`LCSC`, which Fusion surfaces as Manufacturer / MPN+/ supplier fields in the BOM. Omit any and adom-lbr writes the library but prints+a `⛔ INCOMPLETE BOM` reprimand telling the caller to re-run with them.++**RefDes/Value text** (`>NAME`/`>VALUE`) is **carried through from the+`.kicad_mod`** (whatever adom-footprint placed and you tweaked), not invented+here; the linter's `name-value-overlap` check warns when that text lands on the+pads so you can fix it upstream.+ ## Real footprints & 3D models (new in 1.1) 
SKILL.md+38−4@@ -73,8 +73,8 @@ matched as closely as adom-symbol renders it. See [docs/ROADMAP.md](docs/ROADMAP ## Commands ```-adom-lbr export --sym <part.kicad_sym> [--fp <part.kicad_mod>] [--model <part.step>] [--symbol-art <svg>] [--out <dir>] [--targets kicad,fusion,orcad,altium]-adom-lbr generate --sym <part.kicad_sym> [--fp <part.kicad_mod>] [--model <part.step>] [-o out.lbr] [--name <override>]+adom-lbr export --sym <part.kicad_sym> [--fp <part.kicad_mod>] [--model <part.step>] [--symbol-art <svg>] [--out <dir>] [--targets kicad,fusion,orcad,altium] [--mfr <name>] [--mpn <pn>] [--mouser <pn>] [--digikey <pn>] [--lcsc <pn>]+adom-lbr generate --sym <part.kicad_sym> [--fp <part.kicad_mod>] [--model <part.step>] [-o out.lbr] [--name <override>] [--mfr <name>] [--mpn <pn>] [--mouser <pn>] [--digikey <pn>] [--lcsc <pn>] adom-lbr bind-3d (--lbr <part.lbr> | --record-only) (--wip-urn <…fs.file:vf…?version=N> | --urn <…eagle:package…>) [--lineage-urn <…dm.lineage…>] [--package <name>] [--sidecar <part.fusion3d.json>] [-o out.lbr] adom-lbr import <part.lbr> [-o out.kicad_sym] adom-lbr lint <part.lbr> # JSON error codes@@ -114,6 +114,39 @@ the real footprint, and the chip outline are identical across them: exporter: it writes files and hands off; the orchestrating AI does any app-driving. `generate`/`bind-3d` remain for single-format work. +## BOM / supply-chain attributes — the caller MUST pass them++Fusion (and Altium/OrCAD) read **device attributes** for the BOM. adom-lbr writes+them into the `.lbr`'s `<technology>`:++| Flag | EAGLE/Fusion attribute | Fusion field |+|---|---|---|+| `--mfr` | `MF` | Manufacturer |+| `--mpn` | `MPN` | Manufacturer Part Number |+| `--mouser` | `MOUSER` | supplier attribute |+| `--digikey` | `DIGIKEY` | supplier attribute |+| `--lcsc` | `LCSC` | supplier attribute |++**Contract: chip-fetcher looks all of these up, so the caller passes them in.**+`generate`/`export` resolve each value in priority order — explicit flag → a+matching `.kicad_sym` property (`Manufacturer`/`MPN`/`Mouser`/`Digi-Key`/`LCSC`,+common aliases accepted) → auto-fill (`MF` from a sibling+`<mpn>-symbol.extracted.json`, `MPN` from the part name). **When any are still+missing, adom-lbr writes the library anyway but prints a loud `⛔ INCOMPLETE BOM`+reprimand** listing exactly which flags to pass. So if you call adom-lbr without+the part numbers, expect to be told to re-run with them.++## >NAME / >VALUE placement — carried through, not owned++The footprint's RefDes (`>NAME`) and value (`>VALUE`) text positions are **carried+through from the `.kicad_mod`'s own `(fp_text reference …)` / `(fp_text value …)`**+(Y-negated for EAGLE) — so whatever **adom-footprint** places (and the user tweaks+in that app) is exactly what lands in the `.lbr`. adom-lbr does **not** own this.+It only auto-places (clear of the pads) as a fallback when the `.kicad_mod` has no+such text, and it **lints** the result: `name-value-overlap` warns when the text+sits on the pad copper (the "text all over the footprint" problem), telling the+caller to fix the placement upstream in adom-footprint.+ ## generate — the core Parses the `.kicad_sym` (symbol) and, when given, the `.kicad_mod` (footprint),@@ -190,9 +223,10 @@ rotation, length, position; body rectangle from layer-94 wires; pin numbers from ## lint / validate / check -- `lint` — 7 checks, JSON (`passed`, `errors[]`, `warnings[]`): `empty-packages`,+- `lint` — 8 checks, JSON (`passed`, `errors[]`, `warnings[]`): `empty-packages`, `empty-connects`, `device-no-package`, `duplicate-pin-name`,- `connect-pad-missing`, `malformed-xml`, `no-symbol-layer`.+ `connect-pad-missing`, `malformed-xml`, `no-symbol-layer`, and+ `name-value-overlap` (>NAME/>VALUE text sitting on the pad copper). - `validate` — same checks, human-readable; exit 0 valid / 2 errors. - `check` — validates a `.kicad_sym` locally (structure, pins, properties, duplicate names). Run after `import`.
VERSION+1−1@@ -1 +1 @@-1.5.0+1.6.0
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+6−1@@ -1,10 +1,15 @@ { "slug": "adom-lbr",- "version": "1.5.0",+ "version": "1.6.0", "type": "app", "description": "One KiCad part \u2192 every major EDA tool. `adom-lbr export` writes a folder per tool: native KiCad, Fusion 360 (.lbr + 3D handoff), OrCAD (EDIF via adom-orcad), and an Altium bridge hand-off \u2014 real-footprint accurate. Bidirectional KiCad converter + 7-check linter + a live library viewer. Includes a focused fusion-export sub-skill.", "dependencies": {}, "discovery_triggers": [+ "lbr bom attributes",+ "mpn manufacturer part number lbr",+ "mouser digikey lcsc lbr",+ "name value overlap footprint",+ "refdes value placement fusion", "export library all tools", "kicad fusion altium orcad", "multi-tool library export",
page.json+6−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "1.5.0",+ "version": "1.6.0", "type": "app", "description": "One KiCad part → every major EDA tool. `adom-lbr export` writes a folder per tool: native KiCad, Fusion 360 (.lbr + 3D handoff), OrCAD (EDIF via adom-orcad), and an Altium bridge hand-off — real-footprint accurate. Bidirectional KiCad converter + 7-check linter + a live library viewer. Includes a focused fusion-export sub-skill.", "dependencies": {},@@ -33,6 +33,11 @@ "source_visibility": "public", "license": "MIT", "discovery_triggers": [+ "lbr bom attributes",+ "mpn manufacturer part number lbr",+ "mouser digikey lcsc lbr",+ "name value overlap footprint",+ "refdes value placement fusion", "export library all tools", "kicad fusion altium orcad", "multi-tool library export",
src/main.rs+288−10@@ -56,6 +56,21 @@ enum Commands { /// and patch it back into the .lbr. #[arg(long)] model: Option<PathBuf>,+ /// Manufacturer name → device attribute `MF`. chip-fetcher looks this up.+ #[arg(long)]+ mfr: Option<String>,+ /// Manufacturer part number → `MPN`. Defaults to the part name if omitted.+ #[arg(long)]+ mpn: Option<String>,+ /// Mouser part number → `MOUSER`. chip-fetcher looks this up.+ #[arg(long)]+ mouser: Option<String>,+ /// DigiKey part number → `DIGIKEY`. chip-fetcher looks this up.+ #[arg(long)]+ digikey: Option<String>,+ /// LCSC part number → `LCSC`. chip-fetcher looks this up.+ #[arg(long)]+ lcsc: Option<String>, }, /// Pre-write a 3D-model binding into an existing .lbr (fast-path for a /// model that's already cloud-synced). Adds the `<packages3d>` entry + the@@ -133,6 +148,21 @@ enum Commands { /// 3D model (STEP/STP/GLB) → Fusion 3D handoff sidecar #[arg(long)] model: Option<PathBuf>,+ /// Manufacturer name → device attribute `MF`. chip-fetcher looks this up.+ #[arg(long)]+ mfr: Option<String>,+ /// Manufacturer part number → `MPN`. Defaults to the part name if omitted.+ #[arg(long)]+ mpn: Option<String>,+ /// Mouser part number → `MOUSER`. chip-fetcher looks this up.+ #[arg(long)]+ mouser: Option<String>,+ /// DigiKey part number → `DIGIKEY`. chip-fetcher looks this up.+ #[arg(long)]+ digikey: Option<String>,+ /// LCSC part number → `LCSC`. chip-fetcher looks this up.+ #[arg(long)]+ lcsc: Option<String>, /// Chip outline to draw on the schematic symbol (see `generate`) #[arg(long)] symbol_art: Option<PathBuf>,@@ -225,6 +255,54 @@ struct ParsedSymbol { pins: Vec<KicadPin>, body: Option<KicadBody>, group_labels: Vec<GroupLabel>,+ // BOM / supply-chain identifiers, if the source .kicad_sym carried them as+ // properties. Flags (--mfr/--mpn/--mouser/--digikey/--lcsc) override these.+ mfr: String,+ mpn: String,+ mouser: String,+ digikey: String,+ lcsc: String,+}++/// The supply-chain identifiers adom-lbr writes into a device's `<technology>`+/// as EAGLE/Fusion attributes (MF, MPN, MOUSER, DIGIKEY, LCSC). chip-fetcher+/// looks all of these up, so the CONTRACT is: the caller passes them in. When+/// any are missing, adom-lbr writes the library anyway but REPRIMANDS the caller.+#[derive(Debug, Clone, Default)]+struct PartAttrs {+ mfr: Option<String>,+ mpn: Option<String>,+ mouser: Option<String>,+ digikey: Option<String>,+ lcsc: Option<String>,+}++impl PartAttrs {+ /// (EAGLE attribute name, value) for each non-empty field, in BOM order.+ fn pairs(&self) -> Vec<(&'static str, String)> {+ let mut v = Vec::new();+ let mut push = |k: &'static str, o: &Option<String>| {+ if let Some(s) = o { if !s.trim().is_empty() { v.push((k, s.trim().to_string())); } }+ };+ push("MF", &self.mfr);+ push("MPN", &self.mpn);+ push("MOUSER", &self.mouser);+ push("DIGIKEY", &self.digikey);+ push("LCSC", &self.lcsc);+ v+ }++ /// Human labels for the fields still missing — drives the reprimand.+ fn missing(&self) -> Vec<&'static str> {+ let empty = |o: &Option<String>| o.as_ref().map_or(true, |s| s.trim().is_empty());+ let mut m = Vec::new();+ if empty(&self.mfr) { m.push("--mfr (manufacturer)"); }+ if empty(&self.mpn) { m.push("--mpn (mfr part number)"); }+ if empty(&self.mouser) { m.push("--mouser (Mouser part number)"); }+ if empty(&self.digikey) { m.push("--digikey (DigiKey part number)"); }+ if empty(&self.lcsc) { m.push("--lcsc (LCSC part number)"); }+ m+ } } fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Result<ParsedSymbol, String> {@@ -283,12 +361,22 @@ fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Result<ParsedS } let prop = |key: &str| -> String {- let re = regex_lite::Regex::new(&format!(r#"\(property\s+"{}"\s+"([^"]*)""#, key)).unwrap();+ // Property keys can contain spaces/hyphens (e.g. "Mouser Part Number"),+ // so escape regex metacharacters in the key.+ let safe: String = key.chars().flat_map(|c| {+ if "\\.^$|?*+()[]{}".contains(c) { vec!['\\', c] } else { vec![c] }+ }).collect();+ let re = regex_lite::Regex::new(&format!(r#"\(property\s+"{}"\s+"([^"]*)""#, safe)).unwrap(); re.captures(content) .and_then(|c| c.get(1)) .map(|m| m.as_str().to_string()) .unwrap_or_default() };+ // Accept the common aliases each EDA/distributor uses for these fields.+ let prop_any = |keys: &[&str]| -> String {+ for k in keys { let v = prop(k); if !v.trim().is_empty() { return v; } }+ String::new()+ }; Ok(ParsedSymbol { name,@@ -299,6 +387,11 @@ fn parse_kicad_sym(content: &str, name_override: Option<&str>) -> Result<ParsedS pins, body, group_labels,+ mfr: prop_any(&["Manufacturer", "MF", "Mfr", "manufacturer"]),+ mpn: prop_any(&["MPN", "Manufacturer_Part_Number", "Mfr_Part_Number", "Manufacturer Part Number", "mpn"]),+ mouser: prop_any(&["Mouser", "MOUSER", "Mouser Part Number", "Mouser_Part_Number"]),+ digikey: prop_any(&["Digikey", "DigiKey", "Digi-Key", "DIGIKEY", "Digi-Key Part Number", "DigiKey Part Number", "Digikey_Part_Number"]),+ lcsc: prop_any(&["LCSC", "LCSC Part Number", "LCSC_Part_Number"]), }) } @@ -426,6 +519,11 @@ fn parse_eagle_lbr(content: &str) -> Result<ParsedSymbol, String> { pins, body, group_labels: Vec::new(),+ mfr: String::new(),+ mpn: String::new(),+ mouser: String::new(),+ digikey: String::new(),+ lcsc: String::new(), }) } @@ -588,6 +686,11 @@ fn synth_symbol_from_mod(km: &str, name: &str) -> ParsedSymbol { pins, body: Some(KicadBody { left: -7.62, top: body_h / 2.0, right: 7.62, bottom: -body_h / 2.0 }), group_labels: Vec::new(),+ mfr: String::new(),+ mpn: String::new(),+ mouser: String::new(),+ digikey: String::new(),+ lcsc: String::new(), } } @@ -1092,7 +1195,17 @@ fn symbol_art_block(art: &SymbolArt, body: Option<&KicadBody>, scale_frac: f64) } } -fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64) -> String {+// Read a `(fp_text reference|value "…" (at X Y …))` position from a .kicad_mod,+// so adom-lbr can CARRY THROUGH the placement adom-footprint (and the user) set,+// rather than inventing its own. Returns KiCad-space (mm, Y-down) coords.+fn fp_text_pos(km: &str, kind: &str) -> Option<(f64, f64)> {+ let re = regex_lite::Regex::new(+ &format!(r#"\(fp_text\s+{}\s+"[^"]*"\s+\(at\s+([-\d.]+)\s+([-\d.]+)"#, kind)+ ).ok()?;+ re.captures(km).map(|c| (c[1].parse().unwrap_or(0.0), c[2].parse().unwrap_or(0.0)))+}++fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option<&SymbolArt>, symbol_art_scale: f64, attrs: &PartAttrs) -> String { let mut xml = String::with_capacity(8192); xml.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");@@ -1151,8 +1264,30 @@ fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option )); } - xml.push_str(&format!(" <text x=\"0\" y=\"{:.4}\" size=\"1.27\" layer=\"25\" align=\"bottom-center\">>NAME</text>\n", sy2 + 0.5));- xml.push_str(&format!(" <text x=\"0\" y=\"{:.4}\" size=\"1.27\" layer=\"27\" align=\"top-center\">>VALUE</text>\n", sy1 - 0.5));+ // >NAME / >VALUE: CARRY THROUGH the footprint's own reference/value+ // placement, so what the user positions in adom-footprint is exactly what+ // lands in the .lbr (KiCad is Y-down → negate Y). adom-lbr does NOT own+ // this — adom-footprint does, and the user can see/tweak it there. Only+ // when the .kicad_mod carries no such text do we auto-place clear of the+ // pads as a safety net. Either way the linter flags `name-value-overlap`+ // if the result lands on the copper, so a bad placement gets reported back.+ let mut top = sy2;+ let mut bot = sy1;+ for p in &mod_pads {+ let py = -p.y;+ let r = p.dx.max(p.dy) / 2.0; // conservative half-extent (rotation-safe)+ top = top.max(py + r);+ bot = bot.min(py - r);+ }+ let margin = 0.6;+ match fp_text_pos(km, "reference") {+ Some((x, y)) => xml.push_str(&format!(" <text x=\"{:.4}\" y=\"{:.4}\" size=\"1.27\" layer=\"25\" align=\"center\">>NAME</text>\n", x, -y)),+ None => xml.push_str(&format!(" <text x=\"0\" y=\"{:.4}\" size=\"1.27\" layer=\"25\" align=\"bottom-center\">>NAME</text>\n", top + margin)),+ }+ match fp_text_pos(km, "value") {+ Some((x, y)) => xml.push_str(&format!(" <text x=\"{:.4}\" y=\"{:.4}\" size=\"1.27\" layer=\"27\" align=\"center\">>VALUE</text>\n", x, -y)),+ None => xml.push_str(&format!(" <text x=\"0\" y=\"{:.4}\" size=\"1.27\" layer=\"27\" align=\"top-center\">>VALUE</text>\n", bot - margin)),+ } } else { // ── Fallback (no --fp): synthesized two-column placeholder. xml.push_str(&format!(" <description>{} placeholder footprint</description>\n", esc_xml(&sym.name)));@@ -1271,7 +1406,23 @@ fn generate_lbr(sym: &ParsedSymbol, fp_content: Option<&str>, symbol_art: Option } xml.push_str(" </connects>\n");- xml.push_str(" <technologies><technology name=\"\"/></technologies>\n");+ // Supply-chain attributes on the device technology — Fusion 360 surfaces MF+ // as Manufacturer, MPN as Manufacturer Part Number, and the rest as supplier+ // attributes in the BOM. Omitted entirely when nothing was supplied (valid+ // empty technology), so a bare generate still produces a clean library.+ let attr_pairs = attrs.pairs();+ if attr_pairs.is_empty() {+ xml.push_str(" <technologies><technology name=\"\"/></technologies>\n");+ } else {+ xml.push_str(" <technologies>\n <technology name=\"\">\n");+ for (k, v) in attr_pairs {+ xml.push_str(&format!(+ " <attribute name=\"{}\" value=\"{}\" constant=\"no\"/>\n",+ k, esc_xml(&v)+ ));+ }+ xml.push_str(" </technology>\n </technologies>\n");+ } xml.push_str(" </device>\n"); xml.push_str(" </devices>\n"); xml.push_str("</deviceset>\n");@@ -1364,6 +1515,41 @@ pub(crate) fn lint_lbr(content: &str) -> LintResult { warnings.push(LintError { code: "no-symbol-layer".into(), message: "No elements on layer 94 (Symbols) — the symbol body may be invisible".into() }); } + // >NAME / >VALUE placement: warn when they sit on the footprint copper.+ // adom-lbr places them clear of the full pad extent, so this mainly flags+ // hand-made / imported libraries whose ref/value text overlaps the pads+ // (the "text all over the footprint" problem in Fusion).+ {+ let (mut ptop, mut pbot, mut npads) = (f64::MIN, f64::MAX, 0u32);+ let smd_pos = regex_lite::Regex::new(r#"<smd [^>]* y="([-\d.]+)"[^>]*dy="([-\d.]+)""#).unwrap();+ for c in smd_pos.captures_iter(content) {+ let y: f64 = c[1].parse().unwrap_or(0.0);+ let dy: f64 = c[2].parse().unwrap_or(0.0);+ ptop = ptop.max(y + dy / 2.0); pbot = pbot.min(y - dy / 2.0); npads += 1;+ }+ let pad_pos = regex_lite::Regex::new(r#"<pad [^>]* y="([-\d.]+)"[^>]*diameter="([-\d.]+)""#).unwrap();+ for c in pad_pos.captures_iter(content) {+ let y: f64 = c[1].parse().unwrap_or(0.0);+ let d: f64 = c[2].parse().unwrap_or(0.0);+ ptop = ptop.max(y + d / 2.0); pbot = pbot.min(y - d / 2.0); npads += 1;+ }+ if npads > 0 {+ let txt = regex_lite::Regex::new(r#"<text [^>]* y="([-\d.]+)"[^>]*layer="(25|27)"[^>]*>>(NAME|VALUE)</text>"#).unwrap();+ for c in txt.captures_iter(content) {+ let y: f64 = c[1].parse().unwrap_or(0.0);+ if y <= ptop && y >= pbot {+ warnings.push(LintError {+ code: "name-value-overlap".into(),+ message: format!(+ ">{} text sits on the footprint (y={:.2} is within the pad span [{:.2}, {:.2}]) — it will overlap the pads in Fusion. Move it clear of the copper (adom-lbr auto-places it ~0.6mm beyond the outer pad edge).",+ &c[3], y, pbot, ptop+ ),+ });+ }+ }+ }+ }+ LintResult { passed: errors.is_empty(), errors,@@ -1443,6 +1629,86 @@ fn find_adom_orcad() -> Option<String> { // drives Altium/OrCAD/Fusion. OrCAD is produced via the adom-orcad converter // (a CLI, not an app); Altium native is left to its bridge with a staged // hand-off, since "Altium has no headless mode".+// Pull a manufacturer name from a sibling `<mpn>-symbol.extracted.json` (the+// file chip-fetcher writes next to the symbol), so MF can auto-fill when the+// caller didn't pass --mfr.+fn sibling_manufacturer(sym_path: &std::path::Path) -> Option<String> {+ let dir = sym_path.parent()?;+ for entry in std::fs::read_dir(dir).ok()?.flatten() {+ let p = entry.path();+ let Some(name) = p.file_name().and_then(|n| n.to_str()) else { continue };+ if name.ends_with("-symbol.extracted.json") || name == "symbol.extracted.json" {+ if let Ok(txt) = std::fs::read_to_string(&p) {+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(&txt) {+ if let Some(m) = v.get("manufacturer").and_then(|x| x.as_str()) {+ if !m.trim().is_empty() { return Some(m.trim().to_string()); }+ }+ }+ }+ }+ }+ None+}++// Resolve BOM attributes by priority: explicit flag → .kicad_sym property →+// auto-fill (MF from the extracted.json, MPN from the part name). Returns the+// attrs plus transparency notes about anything auto-derived.+#[allow(clippy::too_many_arguments)]+fn resolve_part_attrs(+ parsed: &ParsedSymbol,+ sym_path: Option<&std::path::Path>,+ flag_mfr: &Option<String>, flag_mpn: &Option<String>,+ flag_mouser: &Option<String>, flag_digikey: &Option<String>, flag_lcsc: &Option<String>,+) -> (PartAttrs, Vec<String>) {+ let mut notes = Vec::new();+ let pick = |flag: &Option<String>, prop: &str| -> Option<String> {+ if let Some(f) = flag { if !f.trim().is_empty() { return Some(f.trim().to_string()); } }+ if !prop.trim().is_empty() { return Some(prop.trim().to_string()); }+ None+ };+ let mut mfr = pick(flag_mfr, &parsed.mfr);+ let mpn = pick(flag_mpn, &parsed.mpn);+ let mouser = pick(flag_mouser, &parsed.mouser);+ let digikey = pick(flag_digikey, &parsed.digikey);+ let lcsc = pick(flag_lcsc, &parsed.lcsc);++ if mfr.is_none() {+ if let Some(m) = sym_path.and_then(sibling_manufacturer) {+ notes.push(format!("MF auto-filled from the part's extracted.json: \"{m}\" (pass --mfr to override)"));+ mfr = Some(m);+ }+ }+ let mpn = mpn.or_else(|| {+ let n = parsed.name.trim();+ if n.is_empty() { None } else {+ notes.push(format!("MPN defaulted to the part name \"{n}\" (pass --mpn for the exact orderable part number)"));+ Some(n.to_string())+ }+ });+ (PartAttrs { mfr, mpn, mouser, digikey, lcsc }, notes)+}++// Print what landed in the library, plus a loud REPRIMAND for any part number+// chip-fetcher should have supplied but didn't. Non-fatal: the file is still+// written, but the caller is told to re-run with the missing data.+fn report_bom(attrs: &PartAttrs, notes: &[String]) {+ let have = attrs.pairs();+ if !have.is_empty() {+ let shown: Vec<String> = have.iter().map(|(k, v)| format!("{k}={v}")).collect();+ println!("OK: BOM attributes on the device technology → {}", shown.join(", "));+ }+ for n in notes { println!("HINT: {n}"); }+ let missing = attrs.missing();+ if !missing.is_empty() {+ eprintln!("⛔ INCOMPLETE BOM DATA — the library was written WITHOUT these part numbers:");+ for m in &missing { eprintln!(" {m}"); }+ eprintln!(" chip-fetcher LOOKS ALL OF THESE UP. The caller MUST pass them so the Fusion");+ eprintln!(" library + BOM are complete. Re-run including the missing flags, e.g.:");+ eprintln!(" --mpn <MPN> --mfr \"<Manufacturer>\" --mouser <Mouser#> --digikey <DigiKey#> --lcsc <LCSC#>");+ }+}++#[allow(clippy::too_many_arguments)] fn run_export( sym: Option<PathBuf>, fp: Option<PathBuf>,@@ -1452,6 +1718,11 @@ fn run_export( name: Option<String>, out: Option<PathBuf>, targets: Option<String>,+ mfr: Option<String>,+ mpn: Option<String>,+ mouser: Option<String>,+ digikey: Option<String>,+ lcsc: Option<String>, ) { use std::process::Command; @@ -1491,6 +1762,9 @@ fn run_export( }; let part = parsed.name.clone(); + // ── BOM attributes (chip-fetcher supplies these; reprimand if missing) ──+ let (attrs, attr_notes) = resolve_part_attrs(&parsed, sym.as_deref(), &mfr, &mpn, &mouser, &digikey, &lcsc);+ // ── target selection ── let targets_set: Vec<String> = match &targets { Some(t) => t.split(',').map(|s| s.trim().to_lowercase()).filter(|s| !s.is_empty()).collect(),@@ -1510,7 +1784,7 @@ fn run_export( // Shared EAGLE artifact (Fusion + Altium direct import) + native KiCad sym // (also OrCAD's EDIF input). Build once, reuse.- let lbr = generate_lbr(&parsed, fp_content.as_deref(), symbol_art_resolved.as_ref(), symbol_art_scale);+ let lbr = generate_lbr(&parsed, fp_content.as_deref(), symbol_art_resolved.as_ref(), symbol_art_scale, &attrs); let lint = lint_lbr(&lbr); let kicad_sym_text = match &sym_content { Some(sc) => sc.clone(), // already native KiCad — carry through 1:1@@ -1622,6 +1896,7 @@ fn run_export( } for w in &lint.warnings { println!(" LINT WARN [{}] {}", w.code, w.message); } for e in &lint.errors { println!(" LINT ERROR [{}] {}", e.code, e.message); }+ report_bom(&attrs, &attr_notes); println!("NEXT: open {}/EXPORT.md for per-tool import steps.", out_dir.display()); if model.is_some() { println!("NEXT (Fusion 3D): hand fusion/{part}.fusion3d.json to the Fusion bridge (Create3DPackage → FINISH → Save), then record the minted URN with `adom-lbr bind-3d --record-only`.");@@ -1670,7 +1945,7 @@ fn main() { let cli = Cli::parse(); match cli.command {- Commands::Generate { sym, fp, output, name, model, symbol_art, symbol_art_scale } => {+ Commands::Generate { sym, fp, output, name, model, symbol_art, symbol_art_scale, mfr, mpn, mouser, digikey, lcsc } => { let symbol_art_resolved = symbol_art.as_ref().map(|p| { resolve_symbol_art(p).unwrap_or_else(|e| { eprintln!("ERROR: {e}");@@ -1711,7 +1986,9 @@ fn main() { }; let out_default = sym.as_ref().or(fp.as_ref()).cloned().unwrap_or_else(|| PathBuf::from(&parsed.name));- let lbr = generate_lbr(&parsed, fp_content.as_deref(), symbol_art_resolved.as_ref(), symbol_art_scale);+ // BOM attributes: flags → .kicad_sym props → auto-fill; reprimand if any missing.+ let (attrs, attr_notes) = resolve_part_attrs(&parsed, sym.as_deref(), &mfr, &mpn, &mouser, &digikey, &lcsc);+ let lbr = generate_lbr(&parsed, fp_content.as_deref(), symbol_art_resolved.as_ref(), symbol_art_scale, &attrs); // Lint before writing let lint = lint_lbr(&lbr);@@ -1733,6 +2010,7 @@ fn main() { println!("OK: Generated {} ({} pins, {} bytes)", out_path.display(), parsed.pins.len(), lbr.len()); println!("HINT: Lint {} with {} error(s), {} warning(s)", if lint.passed { "PASSED" } else { "FAILED" }, lint.errors.len(), lint.warnings.len());+ report_bom(&attrs, &attr_notes); // Progressive, moment-of-use hints — surface the pitfalls right where // they bite, so an agent that never read the skill still does the@@ -1789,8 +2067,8 @@ fn main() { } } - Commands::Export { sym, fp, model, symbol_art, symbol_art_scale, name, out, targets } => {- run_export(sym, fp, model, symbol_art, symbol_art_scale, name, out, targets);+ Commands::Export { sym, fp, model, symbol_art, symbol_art_scale, name, out, targets, mfr, mpn, mouser, digikey, lcsc } => {+ run_export(sym, fp, model, symbol_art, symbol_art_scale, name, out, targets, mfr, mpn, mouser, digikey, lcsc); } Commands::Bind3d { lbr, wip_urn, urn, lineage_urn, record_only, package, output, sidecar } => {