← Commit history
Cargo.lockadded+321
@@ -0,0 +1,321 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "adom-lbr"+version = "0.1.4"+dependencies = [+ "anyhow",+ "clap",+ "clap_complete",+ "quick-xml",+ "regex-lite",+ "serde",+ "serde_json",+ "tiny_http",+]++[[package]]+name = "anstream"+version = "1.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"+dependencies = [+ "anstyle",+ "anstyle-parse",+ "anstyle-query",+ "anstyle-wincon",+ "colorchoice",+ "is_terminal_polyfill",+ "utf8parse",+]++[[package]]+name = "anstyle"+version = "1.0.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"++[[package]]+name = "anstyle-parse"+version = "1.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"+dependencies = [+ "utf8parse",+]++[[package]]+name = "anstyle-query"+version = "1.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"+dependencies = [+ "windows-sys",+]++[[package]]+name = "anstyle-wincon"+version = "3.0.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"+dependencies = [+ "anstyle",+ "once_cell_polyfill",+ "windows-sys",+]++[[package]]+name = "anyhow"+version = "1.0.102"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"++[[package]]+name = "ascii"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"++[[package]]+name = "chunked_transfer"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"++[[package]]+name = "clap"+version = "4.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"+dependencies = [+ "clap_builder",+ "clap_derive",+]++[[package]]+name = "clap_builder"+version = "4.6.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"+dependencies = [+ "anstream",+ "anstyle",+ "clap_lex",+ "strsim",+]++[[package]]+name = "clap_complete"+version = "4.6.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"+dependencies = [+ "clap",+]++[[package]]+name = "clap_derive"+version = "4.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"+dependencies = [+ "heck",+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "clap_lex"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"++[[package]]+name = "colorchoice"+version = "1.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"++[[package]]+name = "heck"+version = "0.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"++[[package]]+name = "httpdate"+version = "1.0.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"++[[package]]+name = "is_terminal_polyfill"+version = "1.70.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"++[[package]]+name = "itoa"+version = "1.0.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"++[[package]]+name = "log"+version = "0.4.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"++[[package]]+name = "memchr"+version = "2.8.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"++[[package]]+name = "once_cell_polyfill"+version = "1.70.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"++[[package]]+name = "proc-macro2"+version = "1.0.106"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quick-xml"+version = "0.36.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"+dependencies = [+ "memchr",+ "serde",+]++[[package]]+name = "quote"+version = "1.0.45"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"+dependencies = [+ "proc-macro2",+]++[[package]]+name = "regex-lite"+version = "0.1.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"++[[package]]+name = "serde"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"+dependencies = [+ "serde_core",+ "serde_derive",+]++[[package]]+name = "serde_core"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"+dependencies = [+ "serde_derive",+]++[[package]]+name = "serde_derive"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "serde_json"+version = "1.0.149"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"+dependencies = [+ "itoa",+ "memchr",+ "serde",+ "serde_core",+ "zmij",+]++[[package]]+name = "strsim"+version = "0.11.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"++[[package]]+name = "syn"+version = "2.0.117"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "tiny_http"+version = "0.12.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"+dependencies = [+ "ascii",+ "chunked_transfer",+ "httpdate",+ "log",+]++[[package]]+name = "unicode-ident"+version = "1.0.24"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"++[[package]]+name = "utf8parse"+version = "0.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"++[[package]]+name = "windows-link"+version = "0.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"++[[package]]+name = "windows-sys"+version = "0.61.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"+dependencies = [+ "windows-link",+]++[[package]]+name = "zmij"+version = "1.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
README.md+123−49
@@ -1,87 +1,161 @@ # adom-lbr -**One part → every major EDA tool. A library generator, linter, and converter that takes your part and delivers to Fusion 360, Altium, OrCAD and KiCad — standalone Rust, no local EAGLE.**+EAGLE .lbr generator, linter, validator, and KiCad converter. Converts between KiCad `.kicad_sym` and EAGLE `.lbr` formats so both Fusion 360 and KiCad users can share component libraries. -adom-lbr turns a `.kicad_sym` (and optional `.kicad_mod`) into a true EAGLE `.lbr` with a complete deviceset — symbol + package + pin→pad connects. Because EAGLE libraries are the universal interchange format, that one `.lbr` imports cleanly into **Fusion 360 Electronics, Altium, and OrCAD**; and for KiCad it round-trips back to a native `.kicad_sym`. It can also synthesize a placeable symbol from a footprint's pads when there's no schematic symbol, and verify a library through `service-kicad`. Standalone Rust, no Node, no gallia.+![adom-lbr lint output](docs/screenshot-lint.png) -## Formats & targets+![RP2040 in Fusion 360 — generated by adom-lbr](docs/screenshot-fusion.png) -| Direction | Format | Used by |-|---|---|---|-| **Generate** | EAGLE `.lbr` (symbol + package + connects) | the source of truth adom-lbr builds |-| **Deliver → Fusion 360** | `.lbr` imported directly | Autodesk Fusion 360 Electronics |-| **Deliver → Altium** | `.lbr` import | Altium Designer |-| **Deliver → OrCAD** | `.lbr` import | Cadence OrCAD / Allegro |-| **Deliver → KiCad** | converted to native `.kicad_sym` | KiCad 6+ |-| **Import (reverse)** | EAGLE `.lbr` → `.kicad_sym` | bring EAGLE parts into KiCad |+Standalone Rust — no Node, no gallia, no local EAGLE/Fusion install. Ships a+live, AI-drivable lint-report app (`adom-lbr serve`) alongside the CLI. -The **Send to** bar in the live app pushes the loaded library to whichever of those tools you're running (via the Adom Desktop bridge); the Altium/OrCAD bridges are an open contribution slot.+## Why this exists -## The 3-column library viewer — and an interactive embed+Fusion 360 Electronics shows "No Components" when an `.lbr` file has an empty `<packages>` section or empty `<connects/>`. The old JS generator produced these broken files. adom-lbr generates a **complete** deviceset: symbol + placeholder SMD package + full pin-to-pad connect mappings. -![The Fusion-style 3-column viewer: symbol, footprint, 3D, and the pin→pad mapping table](docs/viewer3col.png)+KiCad users can import any `.lbr` back to `.kicad_sym` with `adom-lbr import`, validated locally (Rust parser). -▶ **[Open the live ADS1115 3-column viewer](https://wiki.adom.inc/api/v1/pages/john/adom-lbr/files/demo/ads1115-viewer.html)** — the exact self-contained file `adom-lbr embed` produces, served straight from this page (pan/zoom each panel; the pin→pad table is fully populated from the datasheet). It is built to sit in an `<iframe>` once the wiki enables component-page iframe embedding.+## Install +```+adompkg install adom-lbr+``` -`adom-lbr embed` exports a **Fusion 360-style three-column viewer** as one self-contained `.html` file: **symbol** in column 1, **footprint** in column 2, the **3D chip** in column 3 — each independently pan/zoomable — with the full **pin → pad mapping table** below, carrying each pin's **type and datasheet description**. It's built to drop straight into an `<iframe>` on a **wiki component page**: a single portable file, no server, no external assets.+## Quick start  ```bash-adom-lbr embed --dir <chip-dir>                    # → <mpn>-lbr-embed.html+# Fusion user: KiCad symbol to EAGLE .lbr+adom-lbr generate --sym RP2040.kicad_sym -o RP2040.lbr++# KiCad user: EAGLE .lbr to KiCad symbol+adom-lbr import RP2040.lbr -o RP2040.kicad_sym+adom-lbr check RP2040.kicad_sym++# Lint any .lbr for Fusion compatibility+adom-lbr lint suspicious.lbr++# Run the live lint-report app (AI-drivable: /state, /load, /eval, /console)+adom-lbr serve --port 8784 --file RP2040.lbr ```-```html-<iframe src="…/<mpn>-lbr-embed.html" width="100%" height="640" style="border:0"></iframe>++## Screenshots++### RP2040 in Fusion 360 Electronics++The generated `.lbr` opens in Fusion with all 57 pins, complete deviceset, and the symbol rendering correctly. The device table shows the package and connect mappings that make Fusion recognize the component.++### Linting the old broken .lbr++The linter catches the exact bugs that cause Fusion to show "No Components":++```json+{+  "passed": false,+  "errors": [+    {"code": "empty-packages", "message": "Packages section is empty"},+    {"code": "empty-connects", "message": "Device has no pin-to-pad connect mappings"},+    {"code": "device-no-package", "message": "Device has no package attribute"}+  ]+} ``` -It assembles the three renders, the pin→pad connects, and the datasheet pin descriptions from a chip directory — exactly the part data adom-symbol, adom-footprint, ds2sf and chip-thumbnailer already produce.+### Round-trip into KiCad 10++The same `.lbr` imported back to `.kicad_sym`, validated locally, and installed as a KiCad 10 symbol library. Renders with all pins, body rectangle, and description.++## Dependencies -## The library at a glance+### adompkg (auto-installed) -![The library viewer: stats and a clean lint pass](docs/lintpass.png)+None — standalone Rust binary. -Load an `.lbr` and the viewer breaks it down at a glance — **symbols, packages, devicesets, pins, pads** — and re-lints it server-side. A green **Lint passed** badge means the library is ready to import into Fusion 360 Electronics with no surprises. The top bar carries the **Send to** targets (KiCad / Fusion / Altium / OrCAD).+### System (manual) -## The linter catches what Fusion rejects+| Dependency | Required? | Purpose |+|---|---|---|+| service-kicad | Optional | Remote validation of `.kicad_sym` via SVG export (no local KiCad install needed) |+| adom-desktop | Optional | Used by adom-symbol's Send to for Fusion 360 delivery (not yet on adompkg) | -![A lint failure: empty pin-to-pad connects](docs/lintfail.png)+**Upstream**: adom-symbol creates the `.kicad_sym` input. chip-fetcher downloads `.lbr` files from manufacturer sites. -This is the whole point. Fusion will silently show "No Components" or refuse to place a part when a library is subtly broken — adom-lbr catches it first. Here the deviceset has **no pin-to-pad connect mappings**, so the linter flags `empty-connects` with a plain-English reason: *"Fusion cannot use this component."* It also catches missing packages, devices with no package attribute, malformed XML, and invisible symbol bodies — each with a code and a fix.+**Downstream**: generated `.lbr` opens directly in Fusion 360 Electronics. Imported `.kicad_sym` installs into KiCad via `kicad_install_library`. -## Generate from the command line+## Features -![Generating an .lbr from KiCad sources](docs/generate.png)+- Bidirectional conversion: `.kicad_sym` to `.lbr` and `.lbr` to `.kicad_sym`+- Generates complete EAGLE `.lbr` with symbol + placeholder SMD package + connect mappings+- Handles duplicate pin names (IOVDD x6, DVDD x2) with EAGLE `@N` suffix convention+- 7-check linter: empty-packages, empty-connects, device-no-package, duplicate-pin-name, connect-pad-missing, malformed-xml, no-symbol-layer+- JSON lint output for programmatic use+- `check` validates `.kicad_sym` locally (Rust parser)+- Shell completions (bash/zsh/fish) -`generate` builds the whole library in one shot from a `.kicad_sym` (+ optional `.kicad_mod`), lint-checks it, and writes a native `.lbr`. The same engine powers the **Send to → Fusion/Altium/OrCAD** flow in adom-symbol and adom-footprint, so whatever you see in those apps is exactly what lands in your library.+## Talk to the AI -## What it does+**Generate:**+- "Generate an lbr from RP2040.kicad_sym"+- "Convert this KiCad symbol to EAGLE format"+- "Make a Fusion 360 library from this symbol" -- **`generate`** — `.kicad_sym` (+ `.kicad_mod`) → native `.lbr`, lint-checked for Fusion compatibility.-- **`lint` / `validate`** — catch the things Fusion rejects: missing connects, incomplete devicesets, bad pad maps.-- **`import`** — EAGLE `.lbr` → KiCad `.kicad_sym`.-- **Live app** — a viewer that lints the loaded `.lbr` and shows symbols / packages / devicesets / pins / pads at a glance.+**Import:**+- "Import this .lbr to KiCad format"+- "Convert EAGLE library to .kicad_sym" -## Send to+**Lint:**+- "Lint this .lbr file"+- "Check if this library will work in Fusion"+- "Why does Fusion show No Components?" -Fusion 360 (open the `.lbr` directly), Altium / OrCAD (download the `.lbr` + import steps; native exporters on the roadmap), KiCad (converted back to `.kicad_sym`).+## Skills -## Open source — check it out, vibe-code it, send it back+| Skill | Purpose |+|---|---|+| adom-lbr | Generate, import, lint, validate, check EAGLE .lbr / KiCad .kicad_sym files | -adom-lbr is MIT and **fully open**. The complete Rust source is in this page's **Files** tab — browse it, or grab `src/` + `Cargo.toml`.+## CLI reference -1. **Build it.** `cargo build --release` drops a standalone binary (no Node, no gallia).-2. **Point your install at your checkout.** `adompkg link john/adom-lbr ./adom-lbr` so the `adom-lbr` you run is your local copy.-3. **Vibe-code a change.** Open the folder in Claude Code and describe what you want — a new lint rule, another export target, a smarter symbol synthesizer. The lint rules live in one readable list, easy to extend.-4. **Send it back.** Open a **pull request** on this page (the wiki reviews and merges per-repo PRs), or start a discussion thread. Good additions get folded into the shipped app.+| Command | Description |+|---|---|+| `generate --sym <file> [-o out] [--fp <file>] [--name <name>]` | KiCad `.kicad_sym` to EAGLE `.lbr` |+| `import <file> [-o out]` | EAGLE `.lbr` to KiCad `.kicad_sym` |+| `lint <file>` | Lint `.lbr`, output JSON with error codes |+| `validate <file>` | Validate `.lbr`, human-readable output |+| `check <file>` | Validate `.kicad_sym` locally (structure, pins, properties) |+| `serve [--port 8784] [--file <lbr>]` | Run the live AI-drivable lint-report app |+| `health` | Print version |+| `install` | Install Claude Code skill + shell completions |+| `completions <shell>` | Generate bash/zsh/fish completions | -## Part of the Adom EDA family+The live app (`serve`) follows the Adom app-creator contract: `GET /state`,+`POST /load` (push raw `.lbr` XML — it's re-linted server-side), `POST /eval` ++`GET /eval/pending` + `POST /eval/:id/result`, `GET/POST /console`,+`POST /shutdown`, `GET /favicon.svg`, and best-effort HD port registration. -Used by **chip-fetcher** and the Send-to flow across **adom-symbol** and **adom-footprint**; pairs with **ds2sf**.+## Build from source -## Run it+The full source ships on the wiki app page — anyone in the community can build+it locally:  ```bash-adom-lbr generate --sym part.kicad_sym --fp part.kicad_mod --name PART --output part.lbr-adom-lbr serve --port 8784          # live linter app (AI-drivable)-adom-lbr embed --dir <chip-dir>     # Fusion-style 3-column viewer as one .html+adompkg source adom-lbr               # downloads + unpacks the repo+cargo build --release                 # standalone — only needs Rust + cargo+cp target/release/adom-lbr ~/.local/bin/ ```-MIT, contributions welcome.++No runtime dependencies — `generate`, `import`, `lint`, `validate`, and `check`+all run fully locally in Rust.++## Contribute back via pull request++adom-lbr is public, source included. To propose a change:++1. `adompkg source adom-lbr` to get the repo.+2. Make your change and `cargo build --release` to confirm it compiles.+3. Open a pull request against the wiki page for `adom-lbr`.++> **Note:** real PR support on wiki.adom.inc is in progress. Until it lands,+> send patches or a branch link to the maintainers and they will apply it.++## Wiki++https://wiki.adom.inc/apps/adom-lbr
VERSIONadded+1
@@ -0,0 +1 @@+0.1.4
bin/adom-lbr
⋯ 1 unchanged line ⋯
docs/icon.svgadded+1
@@ -0,0 +1 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4H9a1.5 1.5 0 0 1 1.5 1.5V20a1.5 1.5 0 0 0-1.5-1.5H5.5A1.5 1.5 0 0 1 4 17z"/><path d="M20 5.5A1.5 1.5 0 0 0 18.5 4H15a1.5 1.5 0 0 0-1.5 1.5V20a1.5 1.5 0 0 1 1.5-1.5h3.5a1.5 1.5 0 0 0 1.5-1.5z"/></svg>
docs/screenshot-fusion.pngadded
⋯ 1 unchanged line ⋯
docs/screenshot-lint.pngadded
⋯ 1 unchanged line ⋯
install.sh+12−2
@@ -1,4 +1,14 @@ #!/bin/bash+# install.sh for adom-lbr (app).+# Runs from the extracted module directory (~/project/adom_modules/adom-lbr/). set -euo pipefail-DIR="$(cd "$(dirname "$0")" && pwd)"; BIN="${HOME}/.local/bin"; mkdir -p "$BIN"-install -m 0755 "$DIR/bin/adom-lbr" "$BIN/adom-lbr"; echo "Installed adom-lbr"++DIR="$(cd "$(dirname "$0")" && pwd)"+cd "$DIR"++# App install: link the binary onto PATH.+BIN_DIR="${HOME}/.local/bin"+mkdir -p "$BIN_DIR"+install -m 0755 "$DIR/bin/adom-lbr" "$BIN_DIR/adom-lbr"+echo "Installed adom-lbr -> $BIN_DIR/adom-lbr"+
package.json+3−19
@@ -1,28 +1,12 @@ {   "slug": "adom-lbr",-  "version": "1.0.3",+  "version": "0.1.4",   "type": "app",-  "description": "Symbol, footprint and 3D side by side with the pin to pad map and datasheet descriptions, exportable as one embeddable file.",+  "description": "EAGLE .lbr generator, 7-check linter, and bidirectional KiCad converter",   "dependencies": {},-  "tags": [-    "eda",-    "kicad",-    "eagle",-    "lbr",-    "fusion360",-    "viewer"-  ],-  "hero": {-    "type": "billboard",-    "headline": "See the part, three ways.",-    "subhead": "Symbol, footprint and 3D side by side with the pin to pad map and datasheet descriptions, exportable as one embeddable file.",-    "screenshot": "screenshots/shot.png",-    "accent": "#00b8b1"-  },   "scripts": {     "install": "./install.sh",     "uninstall": "./uninstall.sh"   },-  "needs_sudo": false,-  "license": "MIT"+  "needs_sudo": false }\ No newline at end of file
release.shadded+87
@@ -0,0 +1,87 @@+#!/bin/bash+#+# Canonical release.sh for an adompkg source repo.+#+# Reads version from package.json, optionally bumps it (--bump patch|minor|major),+# runs build.sh if present, then runs `adompkg publish` and prints the resulting+# package URL.+#+# Usage:+#   ADOMPKG_TOKEN=<carbon-api-key> ./release.sh                # publish current version+#   ADOMPKG_TOKEN=<carbon-api-key> ./release.sh --bump patch   # bump + publish+#+set -euo pipefail++DIR="$(cd "$(dirname "$0")" && pwd)"+cd "$DIR"++if [ ! -f package.json ]; then+  echo "release.sh: no package.json found in $DIR" >&2+  exit 1+fi++REGISTRY="${ADOMPKG_REGISTRY:-https://git-wiki-ktqxite5iglh.adom.cloud}"++if [ -z "${ADOMPKG_TOKEN:-}" ]; then+  if [ -r /var/run/adom/api-key ]; then+    ADOMPKG_TOKEN="$(cat /var/run/adom/api-key)"+    export ADOMPKG_TOKEN+  else+    echo "release.sh: ADOMPKG_TOKEN is required (set ADOMPKG_TOKEN=<carbon-api-key>)" >&2+    exit 1+  fi+fi++# Read current version.+CUR_VERSION=$(node -e "console.log(require('./package.json').version)")+SLUG=$(node -e "console.log(require('./package.json').slug || require('./package.json').name)")++BUMP=""+for a in "$@"; do+  case "$a" in+    --bump)+      shift+      BUMP="${1:-patch}"+      shift || true+      ;;+    --bump=*)+      BUMP="${a#--bump=}"+      shift+      ;;+  esac+done++if [ -n "$BUMP" ]; then+  NEXT=$(node -e "+    const [maj, min, pat] = process.argv[1].split('.').map(n => parseInt(n, 10));+    const b = process.argv[2];+    let out;+    if (b === 'major') out = (maj + 1) + '.0.0';+    else if (b === 'minor') out = maj + '.' + (min + 1) + '.0';+    else out = maj + '.' + min + '.' + (pat + 1);+    console.log(out);+  " "$CUR_VERSION" "$BUMP")+  echo "release.sh: bumping $CUR_VERSION -> $NEXT ($BUMP)"+  node -e "+    const fs = require('fs');+    const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));+    p.version = process.argv[1];+    fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n');+  " "$NEXT"+  CUR_VERSION="$NEXT"+fi++# Optional build step.+if [ -x ./build.sh ]; then+  echo "release.sh: running build.sh"+  ./build.sh+fi++# Publish.+echo "release.sh: publishing $SLUG@$CUR_VERSION to $REGISTRY"+adompkg publish .++# Print published URL.+echo+echo "Published: $REGISTRY/pages/$SLUG"+echo "Install:   adompkg install $SLUG"
src/main.rs+67−13
@@ -1,3 +1,4 @@+mod provenance; mod deliver; mod server; @@ -989,6 +990,17 @@ fn main() {                         process::exit(1);                     }                 }+                // Build the 3-column (symbol|footprint|3D) viewer from the part dir+                // (the .lbr's parent), so GET / shows the full studio, not just lint.+                if let Some(dir) = f.parent() {+                    match build_embed_html(dir) {+                        Ok((html, pins)) => {+                            eprintln!("[adom-lbr] 3-column viewer ready ({pins} pins) from {}", dir.display());+                            server::set_embed(html);+                        }+                        Err(e) => eprintln!("[adom-lbr] no 3-column viewer ({e}); serving lint card"),+                    }+                }             }             if let Err(e) = server::run(port) {                 eprintln!("ERROR: {e:#}");@@ -1062,34 +1074,59 @@ fn read_first(dir: &std::path::Path, names: &[String]) -> Option<String> { }  fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String> {+    let mpn = dir.file_name().and_then(|s| s.to_str()).ok_or("bad --dir")?.to_string();+    let (html, pin_count) = build_embed_html(dir)?;+    let out = out.unwrap_or_else(|| dir.join(format!("{mpn}-lbr-embed.html")));+    fs::write(&out, &html).map_err(|e| format!("writing {}: {e}", out.display()))?;+    println!("OK: 3-column interactive embed ({pin_count} pins) → {}", out.display());+    Ok(())+}++/// Build the 3-column (symbol | footprint | 3D) interactive HTML for a part dir.+/// Reads pre-rendered assets, tolerating both `<mpn>-` prefixed and generic+/// (`symbol.svg`, `assets/footprint.svg`, …) names so the same renders that+/// drive the wiki page also drive the live app and the `embed` export.+pub fn build_embed_html(dir: &std::path::Path) -> Result<(String, usize), String> {     let mpn = dir.file_name().and_then(|s| s.to_str()).ok_or("bad --dir")?.to_string();     // renders-    let sym_svg = read_first(dir, &[format!("{mpn}-symbol.svg")])+    let sym_svg = read_first(dir, &[format!("{mpn}-symbol.svg"), "symbol.svg".into(), "assets/symbol.svg".into()])         .map(|s| s)         .unwrap_or_else(|| "<div class=\"ph\">no symbol render in this directory</div>".into());-    let fp_svg = read_first(dir, &[format!("{mpn}-footprint.authoritative.svg"), format!("{mpn}-footprint.svg")])+    let fp_svg = read_first(dir, &[format!("{mpn}-footprint.authoritative.svg"), format!("{mpn}-footprint.svg"), "footprint.svg".into(), "assets/footprint.svg".into()])         .unwrap_or_else(|| "<div class=\"ph\">no footprint render in this directory</div>".into());     let threed = {-        let cands = ["-3d-iso-named.png", "-3d-iso.png", "-3d-iso-lg.png"]-            .iter().map(|s| dir.join(format!("{mpn}{s}"))).find(|p| p.exists());+        let names = [+            format!("{mpn}-3d-iso-named.png"), format!("{mpn}-3d-iso.png"), format!("{mpn}-3d-iso-lg.png"),+            "3d-iso.png".into(), "assets/3d-iso.png".into(),+        ];+        let cands = names.iter().map(|s| dir.join(s)).find(|p| p.exists());         match cands.and_then(|p| fs::read(&p).ok()) {             Some(bytes) => format!("<img src=\"data:image/png;base64,{}\">", b64(&bytes)),             None => "<div class=\"ph\">no 3D render in this directory</div>".into(),         }     };-    // optional pin→pad connects from a .lbr+    // optional pin→pad connects from a .lbr (tolerate generic name)     let mut pin_to_pad: std::collections::HashMap<String, String> = std::collections::HashMap::new();-    if let Some(lbr) = read_first(dir, &[format!("{mpn}.lbr")]) {+    let mut connect_order: Vec<(String, String)> = Vec::new();+    let lbr_text = read_first(dir, &[format!("{mpn}.lbr"), "library.lbr".into()])+        .or_else(|| {+            // any *.lbr in the dir+            fs::read_dir(dir).ok().and_then(|rd| rd.filter_map(|e| e.ok())+                .find(|e| e.path().extension().map(|x| x == "lbr").unwrap_or(false))+                .and_then(|e| fs::read_to_string(e.path()).ok()))+        });+    if let Some(lbr) = &lbr_text {         if let Ok(re) = regex_lite::Regex::new(r#"<connect gate="[^"]*" pin="([^"]+)" pad="([^"]+)"/>"#) {-            for cap in re.captures_iter(&lbr) {+            for cap in re.captures_iter(lbr) {                 pin_to_pad.insert(cap[1].to_string(), cap[2].to_string());+                connect_order.push((cap[1].to_string(), cap[2].to_string()));             }         }     }     // pins + descriptions from the extracted symbol JSON     let mut pins_json = String::from("[]");     let mut pad_count = fp_svg.matches("adom-pad").count().max(0);-    if let Some(ext) = read_first(dir, &[format!("{mpn}-symbol.extracted.json")]) {+    if let Some(ext) = read_first(dir, &[format!("{mpn}-symbol.extracted.json"), "symbol.extracted.json".into(), "assets/symbol.extracted.json".into()]) {         if let Ok(v) = serde_json::from_str::<serde_json::Value>(&ext) {             if let Some(arr) = v.get("pins").and_then(|p| p.as_array()) {                 let rows: Vec<serde_json::Value> = arr.iter().map(|p| {@@ -1112,19 +1149,36 @@ fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String>             }         }     }+    // fallback: no extracted JSON — build the pin→pad rows straight from the .lbr connects+    if pins_json == "[]" && !connect_order.is_empty() {+        let rows: Vec<serde_json::Value> = connect_order.iter().map(|(pin, pad)| {+            serde_json::json!({ "number": pad, "name": pin, "pad": pad, "type": "Signal", "color": "#80cbc4", "desc": "" })+        }).collect();+        pad_count = pad_count.max(rows.len());+        pins_json = serde_json::to_string(&rows).unwrap_or_else(|_| "[]".into());+    }     let pin_count = serde_json::from_str::<serde_json::Value>(&pins_json).ok()         .and_then(|v| v.as_array().map(|a| a.len())).unwrap_or(0); +    // Display name: prefer the .lbr's deviceset/symbol name over the dir name.+    let display_name = lbr_text.as_deref()+        .and_then(|l| ["deviceset", "symbol", "package"].iter().find_map(|t| {+            let n = format!("<{t} ");+            let i = l.find(&n)? + n.len();+            let r = &l[i..];+            let k = r.find("name=\"")? + 6;+            let j = r[k..].find('"')? + k;+            Some(r[k..j].to_string())+        }))+        .filter(|s| !s.is_empty())+        .unwrap_or_else(|| mpn.clone());     let html = include_str!("embed.html")-        .replace("__NAME__", &esc_html(&mpn))+        .replace("__NAME__", &esc_html(&display_name))         .replace("__SYM__", &sym_svg)         .replace("__FP__", &fp_svg)         .replace("__THREED__", &threed)         .replace("__PINS__", &pins_json)         .replace("__PADN__", &pad_count.to_string())         .replace("__PINN__", &pin_count.to_string());-    let out = out.unwrap_or_else(|| dir.join(format!("{mpn}-lbr-embed.html")));-    fs::write(&out, &html).map_err(|e| format!("writing {}: {e}", out.display()))?;-    println!("OK: 3-column interactive embed ({pin_count} pins) → {}", out.display());-    Ok(())+    Ok((html, pin_count)) }
src/provenance.rsadded+73
@@ -0,0 +1,73 @@+//! Canonical source-provenance LABELING — the single rule for how every Adom+//! EDA app names where a symbol / footprint / 3D / datasheet actually came from.+//!+//! THE RULE (John, 2026-06-23): label by the DISCOVERY SOURCE — where the file+//! was actually downloaded — NOT by content_origin (who authored the model).+//!+//!   - ONLY the maker's OWN website counts as "the manufacturer", and then we+//!     NAME the site: "Texas Instruments (ti.com)", "Analog Devices (analog.com)".+//!   - The moment the manufacturer hands you off to a 3rd-party library —+//!     UltraLibrarian, SnapMagic/SnapEDA, Component Search Engine — that+//!     AGGREGATOR is the source. NEVER label it "Manufacturer".+//!   - A fuzzy bare "Manufacturer" is misleading and banned.+//!+//! This module is mirrored (same mapping) in adom-symbol, adom-footprint and+//! adom-lbr so the label is identical everywhere. See the `chip-provenance-labeling` skill.++/// The maker's canonical website domain, for "Texas Instruments (ti.com)" labels.+/// Matched case-insensitively on a substring of the manufacturer name.+pub fn mfr_domain(manufacturer: &str) -> Option<&'static str> {+    let m = manufacturer.to_lowercase();+    let table = [+        ("texas instr", "ti.com"), ("analog dev", "analog.com"),+        ("stmicro", "st.com"), ("nxp", "nxp.com"), ("microchip", "microchip.com"),+        ("infineon", "infineon.com"), ("on semi", "onsemi.com"), ("onsemi", "onsemi.com"),+        ("nordic", "nordicsemi.com"), ("espressif", "espressif.com"),+        ("renesas", "renesas.com"), ("diodes inc", "diodes.com"), ("vishay", "vishay.com"),+        ("bosch", "bosch-sensortec.com"), ("maxim", "analog.com"), ("dialog", "renesas.com"),+        ("rohm", "rohm.com"), ("toshiba", "toshiba.com"), ("skyworks", "skyworksinc.com"),+        ("qorvo", "qorvo.com"), ("micron", "micron.com"), ("cypress", "infineon.com"),+        ("silicon lab", "silabs.com"), ("silabs", "silabs.com"), ("lattice", "latticesemi.com"),+        ("winbond", "winbond.com"), ("ti ", "ti.com"),+    ];+    table.iter().find(|(k, _)| m.contains(k)).map(|(_, v)| *v)+}++/// The human label for a source, given the discovery-source slug + the+/// manufacturer name. THIS is the function every app calls.+pub fn source_label(discovery: &str, manufacturer: &str) -> String {+    match discovery.to_lowercase().as_str() {+        // Manufacturer-DIRECT: only when downloaded from the maker's own site.+        "manufacturer" | "mfr" | "vendor" => {+            if let Some(dom) = mfr_domain(manufacturer) {+                if manufacturer.is_empty() { format!("Manufacturer · {dom}") }+                else { format!("{manufacturer} ({dom})") }+            } else if !manufacturer.is_empty() {+                format!("{manufacturer} (direct)")+            } else {+                "Manufacturer (direct site)".to_string()+            }+        }+        // 3rd-party aggregators — NEVER "the manufacturer".+        "cse" | "componentsearch" | "cse_ul" | "samacsys" => "Component Search Engine".into(),+        "snapeda" | "snapmagic" => "SnapEDA".into(),+        "ultralibrarian" | "ul" => "Ultra Librarian".into(),+        "mouser" => "Mouser".into(),+        "digikey" => "DigiKey".into(),+        "arrow" => "Arrow".into(),+        "lcsc" => "LCSC".into(),+        "kicad_community" => "KiCad community".into(),+        other => other.to_string(),+    }+}++/// A short note for the "the manufacturer linked you to it, but it's a 3rd+/// party" middle-ground — appended in tooltips so the user knows the maker+/// pointed here even though the file came off an aggregator.+pub fn linked_from_mfr_note(manufacturer: &str) -> String {+    if manufacturer.is_empty() {+        "the manufacturer's page linked to this 3rd-party library".to_string()+    } else {+        format!("{manufacturer}'s own page links here, but the file is hosted by this 3rd-party library — not downloaded from {manufacturer} directly")+    }+}
src/server.rs+15−1
@@ -28,6 +28,8 @@ pub struct AppState { }  static STATE: Mutex<Option<AppState>> = Mutex::new(None);+/// Filled 3-column (symbol | footprint | 3D) HTML, served at `/` when present.+static EMBED_HTML: Mutex<Option<String>> = Mutex::new(None); static EVAL_PENDING: Mutex<VecDeque<(u64, String)>> = Mutex::new(VecDeque::new()); static EVAL_RESULTS: Mutex<Option<HashMap<u64, Value>>> = Mutex::new(None); static CONSOLE: Mutex<Option<Vec<String>>> = Mutex::new(None);@@ -40,6 +42,11 @@ pub fn set_state(s: AppState) {     *STATE.lock().unwrap() = Some(s); } +/// Install the 3-column viewer HTML so `GET /` shows symbol|footprint|3D live.+pub fn set_embed(html: String) {+    *EMBED_HTML.lock().unwrap() = Some(html);+}+ /// Parse + lint an `.lbr` (EAGLE XML) into the displayable app state. pub fn analyze_lbr(content: &str) -> AppState {     let lint = crate::lint_lbr(content);@@ -153,7 +160,14 @@ fn hd_post(body: &str) {  fn route(method: &Method, url: &str, body: &str) -> Response<std::io::Cursor<Vec<u8>>> {     match (method, url) {-        (Method::Get, "/") | (Method::Get, "/index.html") => html(APP_HTML),+        (Method::Get, "/") | (Method::Get, "/index.html") => {+            // 3-column viewer when render assets are loaded; lint card otherwise.+            match &*EMBED_HTML.lock().unwrap() {+                Some(h) => html(h),+                None => html(APP_HTML),+            }+        }+        (Method::Get, "/lint") => html(APP_HTML),         (Method::Get, "/favicon.svg") => svg(FAVICON),         (Method::Get, "/state") => json_resp(&state_json()),         (Method::Post, "/load") => {
uninstall.sh+5−1
@@ -1,3 +1,7 @@ #!/bin/bash+# uninstall.sh for adom-lbr (app). set -euo pipefail-rm -f "${HOME}/.local/bin/adom-lbr"; echo "Uninstalled adom-lbr"++rm -f "${HOME}/.local/bin/adom-lbr"+echo "Uninstalled adom-lbr"+