---
name: adom-chipfit
description: >
  Validate a 3D chip GLB against its KiCad footprint — pin-1 alignment,
  package-family guard, seat-plane Δz, size-match, and persistent pin-1
  registration bake. Use when the user wants to check that a chip model
  matches its footprint before committing it to a library, after
  generating a new 3D part, or when a chip looks "off" on an InstaPCB
  preview. Trigger words: chipfit, chip fit, validate chip, validate
  footprint, check 3d chip, check footprint, footprint alignment, pin-1
  alignment, pin 1 check, register pin 1, bake pin-1, mark pin 1, chip
  on pads, chip-on-pads viewer, seat plane, z offset chip, lqfp
  alignment, qfn alignment, DIP alignment, 3d footprint check.
---

# adom-chipfit — 3D Chip ↔ KiCad Footprint Validator

Pairs a `.kicad_mod` footprint with a `.glb` 3D chip model in a
Hydrogen webview tab, runs the full validation stack, and lets the
engineer bake a persistent pin-1 registration mark onto the chip.

## Run it

```
adom-chipfit check \
  --footprint LQFP-32_7x7mm_P0.8mm.kicad_mod \
  --glb 3d-lqfp-32.glb
```

A Hydrogen tab named "chipfit" opens on the focused pane. The HUD
reports everything the engineer needs to sign off on the pair:

- **Pads** — parsed from the `.kicad_mod`
- **FP bbox** — footprint in-plane extents
- **GLB bbox** — chip extents in mm (after ×1000 scale-up from the
  glTF default metre unit)
- **Pin-1 pad** — KiCad X,Y of pin 1
- **FP origin** — always (0, 0) by convention
- **|FP bbox / GLB bbox|** — size-match ratio; >10 % deviation
  amber, >100 % red (wrong-size pair)
- **Seat-plane Δz** — interpreted offset between the chip's bottom
  and the pad surface. Near-zero for SMD = seated; ≈ −1.6 mm for
  thru-hole = leads clear a standard FR4 board.

## ⚠️ Body-width disambiguation — never guess narrow vs wide

This is the failure mode that wastes the user's day: pin-1 alignment
passes, package family agrees ("both SOIC-8"), seat-Δz is fine, but
the chip's lead feet land **inboard or outboard** of the footprint
pads. The board would solder, but the joints look wrong on every
photo and the human-eye composite makes it obvious.

The root cause is always the same — multiple "same-family same-pin-
count" packages exist with **different body widths**, and the
auto-fetcher silently picked the wrong one:

| Family | Variants | Pad-spacing X-center (typ) |
|---|---|---|
| **SOIC-8 narrow** (3.9 × 4.9 mm, "150-mil") | most ICs in this size are SI8xxx, MAX2xx, ATtiny, AT24Cxx | ±2.65 mm |
| **SOIC-8 wide** (5.3 × 5.3 mm, "208-mil") | W25Q-series flash, M95-series EEPROM | ±3.65 mm |
| **SOIC-8 wide** (5.3 × 6.2 mm, "300-mil") | older industrial parts, optocouplers | ±4.20 mm |
| **DIP-8** narrow (300-mil pitch) vs **DIP-8** wide (600-mil pitch) | every through-hole IC since 1970 | very different |
| **SSOP-N** vs **TSSOP-N** (same pin count, different pitch) | mixed, watch lead pitch | depends on N |
| **QFP** family — TQFP / LQFP / PQFP | body width sets pad ring radius | depends on N |

Trigger signals in the metadata `package` string:

- **Narrow**: `3.9x4.9`, `150-mil`, `150 mil`, `narrow`, body width < 5 mm
- **Wide 208-mil**: `5.3x5.3`, `208-mil`, `208 mil`, body width ≈ 5.3 mm
- **Wide 300-mil**: `5.3x6.2`, `300-mil`, `300 mil`, `wide`, body width ≥ 6 mm

**The rule for chipfit + aci 3d fetch:** if the package string
doesn't carry one of the dimension signals above, **REFUSE** to
auto-pick a body. Fall through to the human-assist tier and let the
user hand over a vendor STEP that matches the actual part. Silent
guess = silent damage. The cost of refusing is ~30 s of user
attention; the cost of guessing wrong is a 5-hour debugging session
ending in "this fp does not line up to that chip correctly."

The auto-pick logic lives in [adom-circuit/bin/aci/src/main.rs](../adom-circuit/bin/aci/src/main.rs)
`soic_dip_qfp_disambiguate()`. When you add a new family there,
include the disambiguation rule from the start. Don't ship a `match`
arm that maps "QFP-32" to one specific body without checking width.

## Deterministic pre-launch guards (the alarm bells)

Chipfit refuses to launch the viewer if any of these checks
disagree. Order: cheapest first, hardest last. Each one prints
`✓ guards: <check>` on pass; on fail, prints a multi-line error
with the inputs and a "what to do next" sentence, then exits
non-zero. None of them is bypassable. User feedback that wrote
this section (2026-04-26): *"holy fuck did you even test the
amount of pins on the chip vs the amount of pins on the fp? …
this is a precise world we are living in with electronics
prototyping. never fuck just guess. test every possible
deterministic thing you can. you CAN'T FUCK THIS STEP UP."*

| # | Check | Inputs | What it catches |
|---|---|---|---|
| 1 | Family compatibility (filename) | parse `QFN-56` / `LQFP-48` etc from FP + GLB filenames | obvious wrong-family pairs (an LQFP-48 footprint with a QFN-24 chip GLB) |
| 2 | **Pin-count match** | `(pad N` count from `.kicad_mod`, classified into numbered / EP / mounting; package designator from GLB root node name (e.g. `QFN-56-1EP_7x7mm_P0.4mm_EP3.2x3.2mm`) | the bug we shipped: a 56-pin chip in a 61-pad FP, or any other count drift. Has ±5 slack when the GLB declares an EP (some FPs split EP into thermal-via subpads). **EXACT match required for non-EP packages.** |
| 3 | **Body-dim envelope** | parse `<W>x<H>mm` from GLB root node name; pad envelope (`min_x`, `max_x`, `min_y`, `max_y`) from `.kicad_mod` | narrow-vs-wide-body bug — pad envelope must be ≥ chip body × 0.85 and ≤ chip body × 1.6 + 4 mm |

The pin-count guard is the strongest. The GLB root node name from
kicad-packages3d carries the canonical package designator; combining
that with the `.kicad_mod` pad classification gives a hard ground
truth that filename heuristics never had.

If you add a new chipfit guard, FIRST: ask "is this deterministic on
just the .kicad_mod text + GLB JSON header + (optional) metadata.json?"
If yes, add it here and refuse on mismatch. Don't ship a check that
"warns but lets you through" — silence is what burned us before.

Outstanding deterministic checks that aren't yet wired (TODO):
- Pad pitch vs `P<num>mm` parsed from GLB name (catches SSOP vs TSSOP pitch mix-ups).
- Pin-1 corner consistency (chip's pin-1 marker quadrant vs `.kicad_mod` pad 1 quadrant).
- Thermal-pad presence — if GLB declares `-1EP`, FP must have either an EP-named pad OR a centered numbered pad.

## Three guardrails

1. **Package-family + pin-count mismatch refusal.** `adom-chipfit`
   parses package identity from both filenames (LQFP-48 / QFN-24 /
   TSSOP-14 / DIP-8 / …). If they don't agree on family (alias
   groups: LQFP/TQFP/QFP, QFN/DFN, TSSOP/SSOP/SOIC/SOP/SO, DIP/PDIP,
   SOT/SOD, BGA/CSP) or pin count differs, the CLI errors out
   **before** binding the server and the browser shows a blocking
   red panel. An LQFP-48 footprint cannot physically assemble a
   QFN-24 chip; "aligning" them anyway produces a misleading
   screenshot.

2. **GLB origin-convention classifier.** Some chip GLBs (Fusion 360 /
   SolidWorks exports) are authored with z = 0 at the bottom of a
   1.6 mm board instead of at the top copper plane. Rather than
   flagging those as "broken", the viewer reads the offset: ±1
   board-thickness (~1.6 mm) is labelled as a convention mismatch
   and the chip is auto-aligned for display, with the raw offset
   and the shift applied both surfaced in the HUD. Anything outside
   those bands (e.g. +0.5 mm) is flagged as a genuine float/sink.

3. **Two-stage top-surface bbox.** The chip's in-plane bounding box
   includes leads; the pin-1 registration dot must sit on the
   **body**, not on a lead. `adom-chipfit` finds the overall
   chipMaxZ, then isolates the top-surface bbox (meshes whose
   maxZ is within 50 µm of chipMaxZ) before snapping to the corner
   nearest pin 1. For an LQFP-32 that's 6.85×6.85 mm plateau vs
   9×9 mm overall.

## Persistent pin-1 registration

Click **Bake pin-1 → GLB** once the three sign-off checks are green.
The app:

1. Resets the scene wrapper's scale/position so Babylon exports in
   native GLB units.
2. Adds a flat red disc (0.25–0.55 mm diameter, 80 µm tall — real
   IC pin-1-mark scale) at the top-surface corner nearest pin 1,
   inset ~11 % of the plateau minor dim so the dot sits cleanly on
   the body.
3. Uses Babylon's GLTF2Export to write `<stem>-pin1.glb` alongside
   the source. Original is backed up to `<stem>.glb.bak` on first
   bake.

Once baked, every downstream tool that loads the chip —
`adom-tsci`, `basic-3d-viewer`, InstaPCB preview, molecule review —
renders the dot automatically. The chip becomes self-identifying
across every future project the user builds with it.

## AI-drivable endpoints

Every UI action has a matching HTTP endpoint (per `app-creator` §7):

```
GET  /meta                  → { footprint_name, glb_name, port }
GET  /check                 → latest HUD snapshot (pushed by UI)
POST /check                 → UI writes its snapshot here
GET  /camera-pending        → server→UI camera command channel
POST /camera                → { view: top|front|right|iso } or
                              { alpha, beta, radius }
POST /bake-pin1             → body = raw model/gltf-binary; writes
                              next to the source glb with -pin1 suffix
POST /shutdown              → clean exit
```

## Provenance

Open the HUD's **▸ Provenance** panel to see the full pipeline for
the current view:
- source file names + sizes
- parsed pad count
- glTF node→mesh map; which nodes were flagged as baked-PCB and hidden
- wrapper scaling (×1000 for m→mm)
- raw chipMinZ; applied auto-align shift
- pin-1 bake: every stage's bbox + applied coords

Also available via `curl $PORT/check | jq -r .provenance`.

## Skill integration

- Defaults to port **8894**. Override with `--port`.
- Tab name defaults to **"chipfit"**. Override with `--tab-name`.
- `--no-tab` runs the server without opening a Hydrogen tab (useful
  for scripts that open the URL in a separate pup window on the
  desktop).

## Installation

Installed from the wiki via the one-liner:

```
curl -fsSL https://wiki-ufypy5dpx93o.adom.cloud/static/apps/adom-chipfit/adom-chipfit -o /usr/local/bin/adom-chipfit
chmod +x /usr/local/bin/adom-chipfit
adom-chipfit install
```

The `install` subcommand fetches the latest `SKILL.md` +
`BUILD-SKILL.md` from the wiki and writes them to
`~/.claude/skills/`. Bash completions are deployed to
`~/.local/share/bash-completion/completions/adom-chipfit`.

## Upstream gap: when there is no real chip GLB to validate

A chipfit run is only as good as its inputs. If `--glb <path>`
points at a stub (sub-2 KB GLB with no mesh) or an AI-extruded
geometry that doesn't match the datasheet mechanical drawing,
chipfit either refuses to launch (package-family guard) or shows
empty pads + no chip body — useless for pin-1 alignment validation.

The right answer is **upstream**: get a real vendor-published
3D model into the GLB before running chipfit. See
[adom-circuit/PLAN.md Part 30.10 §"3D source priority — vendor-
fetch with human-assist"](../adom-circuit/PLAN.md) for the canonical
flow:

1. `aci 3d fetch <mpn>` — tries `service-kicad model fetch` for
   generic packages (SOIC-8, QFN-56, SOT-23-5, etc.).
2. If that fails: prints a two-paragraph report (what we tried,
   why it failed) plus the most-likely vendor URL (ST product
   page, TI product page, Ultra Librarian, Mouser
   ComponentSearchEngine, etc.).
3. `aci 3d fetch <mpn> --launch-pup` opens those URLs in
   **persistent pup sessions** (`pup browser_open_window
   --session aci-vendor-cad-<vendor>`) — the user logs into the
   vendor *once*, the cookies persist, future fetches skip the
   login. Per-vendor session keeps cookie jars isolated.
4. User downloads the STEP/STP from the vendor in the pup window.
5. `aci 3d ingest <mpn> ~/Downloads/<file>` converts via
   `service-step2glb` and installs the GLB.
6. THEN run `adom-chipfit check` to validate.

For Adom users, the rule of thumb: if `aci library` shows an amber
"AI-extruded — vendor model exists at <url>" caveat in the `glb`
provenance bar, fetch the vendor model before validating with
chipfit. AI-extruded GLBs pass chipfit's package-family guard
(they're sized to the datasheet body envelope) but they lack the
fine details — pin texture, cavity positions, marking dot — that
make the chipfit composite valuable downstream.

## Downstream artifacts — emitted natively at bake time

A successful chipfit bake (interactive `Bake pin-1 → GLB` click OR
non-interactive `adom-chipfit bake`) now writes TWO artifacts in
one shot:

| Artifact | Consumer | Format |
|---|---|---|
| `<part>-3d-pin1.glb` | adom-tsci, basic-3d-viewer, InstaPCB preview, molecule review — "just the chip" with pin-1 dot baked in | GLB, native metres, chip-only |
| `<part>-chipfit-composite.glb` | aci library hero `baked` stage — chip on pads with silkscreen, rotating as one unit | GLB, native metres, chip + pads + silkscreen + pin-1 dot |

The composite is a true Babylon-scene export. The browser-side
`bakePin1` function (`src/ui.html`) sets up two filters:

1. **chip-only filter** — `keep` set built from `chipMeshes +
   marker`, exported as `-pin1.glb`.
2. **composite filter** — `keepComposite` set adds pad and silk
   meshes (and walks ancestors INTO `keepComposite`, NOT the outer
   chip-only `keep` set — that bug ate 5 hours one afternoon).

Both exports go through `BABYLON.GLTF2Export.GLBAsync` against the
same scene; the second one POSTs to `/bake-composite`, which writes
the file alongside `-pin1.glb`. The non-interactive `adom-chipfit
bake` subcommand passes `?auto-bake=1` so the headless run clicks
the bake button automatically and shuts down on completion.

### Unit-scale gotcha — keep this fix

The chip is loaded under a wrapper TransformNode whose scaling is
(1000, 1000, 1000) — that's how a metres-native GLB renders in the
millimetre-scene. Pads and silkscreen are top-level meshes in mm.
Just before the pin-1 export, the wrapper is reset to scale 1 so the
chip-only GLB exports in native metres. That reset BREAKS the
composite export if you don't account for it: chip would end up
1000× smaller than the pads in the composite GLB.

The fix: for the composite export ONLY, scale the pads + silk DOWN
by 0.001× (positions and dimensions) so they re-enter the chip's
native-metre space. Restore in a `finally` block:

```javascript
const padSilkSaved = padSilkMeshes.map(m => ({
  mesh: m, scale: m.scaling.clone(), pos: m.position.clone(),
}));
try {
  padSilkMeshes.forEach(m => {
    m.scaling = m.scaling.scale(0.001);
    m.position = m.position.scale(0.001);
  });
  // … export composite …
} finally {
  padSilkSaved.forEach(({mesh, scale, pos}) => {
    mesh.scaling = scale; mesh.position = pos;
  });
}
```

The "scale chip up to mm instead" alternative was tried first and
**doesn't work** — Babylon's exporter writes the wrapper transform in
a way that confuses model-viewer's auto-camera framing, leaving the
viewport empty.

### Still TODO (lower priority, tracked in PLAN Part 30.10)

| Artifact | Consumer | Why useful |
|---|---|---|
| `<part>-chipfit-iso.png` | wiki page hero, fallback when model-viewer doesn't load | PNG, isometric, 600×450, dark background |
| `<part>-chipfit-top.png` | wiki page top-down + DRC hover | PNG, top-down ortho, 600×450 |
| `<part>-chipfit-fits.json` | aci-board placement validator | `{pin1_pos, pad_count, body_bbox, silkscreen_outline}` |

Render PNGs via `service-step2glb screenshot` (shared GLB→PNG
renderer) — keeps the chipfit binary thin.

## Optional add-on layers in the baked composite

The composite GLB ships with **two off-by-default scene-graph layers**
that downstream board-build tools (and the user via the chipfit
scene-graph HUD) can flip on without re-running chipfit. Both are
parented under `chipfit-pads` so the existing
`addUnder(V2.padsGroup)` filter pulls them into the export
automatically.

### `chipfit-vias-heatsink` — EP heatsink via array

Exposed-pad packages (QFN, QFP, DFN, BGA-with-thermal-pad) get most
of their thermal performance from heat-sinking copper into inner
ground planes via an array of through-vias under the EP. Drilling
these by hand for every part the engineer touches is tedious — and
it's the kind of "obvious add-on" they'd appreciate the tool doing
for them.

| Default | Value |
|---|---|
| Via diameter | **0.3 mm** |
| Pitch         | **0.8 mm** (0.5 mm copper between adjacent drills) |
| Edge inset from EP | **0.4 mm** |
| Coverage      | as many as fit on the pitch grid centred on the EP |

For an RP2040 EP (3.2 × 3.2 mm) the default produces a 4 × 4 = 16
via array. For a larger EP (e.g. 5.6 × 5.6 mm) the same rule yields
~7 × 7 = 49 vias.

EP detection: the largest pad whose centre is within 0.5 mm of the
part origin AND whose long side is ≥ 1 mm (smaller centred pads are
treated as ordinary signal pads, not thermal). Disable / customise
by deleting / replacing the `chipfit-vias-heatsink` node before
export.

The TransformNode carries metadata so a downstream tool can reason
about it without re-walking the scene:

```js
{
  layer: 'ep-heatsink-vias',
  via_diameter_mm: 0.3,
  pitch_mm: 0.8,
  ep_pad: { number, x_mm, y_mm, w_mm, h_mm },
  note: 'Off by default. Toggle on for thermal heatsink to inner ground plane.',
}
```

### `chipfit-solder-jets` — InstaPCB jet-dispense pattern

Adom's PCBA line uses a **solder-jetting head** rather than a stencil
— it ejects 300 µm dome-shaped solder droplets onto each pad. The
chipfit composite includes the dispense pattern as half-spheres
sitting on top of the pads so the engineer can preview what the
machine will actually deposit.

| Rule | Value |
|---|---|
| Droplet diameter | **300 µm** |
| Min spacing **same pad** (centre-to-centre) | dot-edge to dot-edge ≥ **100 µm** (`pitch = 400 µm`) |
| Min spacing **across pads** (centre-to-centre) | dot-edge to dot-edge ≥ **300 µm** (oversplatter buffer) |
| Coverage floor  | **always ≥ 1 dot per pad** — even if the pad is < 300 µm in one axis. Solder wicks during reflow; under-deposition starves the joint. |

#### Why the two spacings are different
*Inside* one pad, the droplets fuse during reflow into a single
solder volume — 100 µm spacing is just enough to keep the dispenser
laying down distinct shots. *Between* pads, the droplets must NOT
fuse before reflow, otherwise solder bridges appear; the 300 µm
inter-pad rule gives the jet room to overshoot ±150 µm and still
land cleanly. On 0.4 mm pitch QFN, this means alternating pads get
dots — that's a real process limit, not an algorithm bug.

The TransformNode metadata mirrors the rule set:

```js
{
  layer: 'solder-jet-dots',
  droplet_diameter_mm: 0.3,
  intra_pad_min_spacing_mm: 0.1,
  inter_pad_min_spacing_mm: 0.3,
}
```

See [`gallia/skills/solder-jetting/SKILL.md`](../gallia/skills/solder-jetting/SKILL.md)
for the underlying process spec, plus the
[`solder-jet-sizer`](../gallia/skills/solder-jet-sizer/) skill for
the interactive 2D widget version of the same calculation.

## No garbage links — every user-clickable URL gets probed before shipping

User-facing URLs that we GENERATE (vendor search-result pages,
manufacturer product pages, anything templated from an MPN or a
keyword) MUST be probed at boot time and the UI MUST gracefully
hide / disable any URL whose probe failed.

User feedback 2026-04-29: *"when i click the photo or search link
it gives me zero results. have you even tested yourself? it would
seem like you would test all of this even when the normal adom
user is using this feature so you aren't giving them back garbage
links."*

### What "templated URL" means

URLs we constructed by string-substitution from a parameter we
control: `https://www.mouser.com/c/?q=${MPN}`,
`https://www.digikey.com/en/products/result?keywords=${MPN}`,
`https://www.lcsc.com/search?q=${MPN}`, etc.

URLs we got back from a verified API (Mouser's `product_url`,
DigiKey's `product_url`, LCSC product-detail URLs) are NOT
templated — they're authoritative and pass through.

### Deterministic startup probe

```js
const URL_PROBES = {
  'mouser-search':  `https://www.mouser.com/c/?q=RP2040`,
  'digikey-search': `https://www.digikey.com/en/products/result?keywords=RP2040`,
  'lcsc-search':    `https://www.lcsc.com/search?q=RP2040`,
};
const URL_PROBE_RESULTS = {};
async function probeUrls() {
  for (const [name, url] of Object.entries(URL_PROBES)) {
    try {
      const r = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(8000) });
      URL_PROBE_RESULTS[name] = { ok: r.ok, status: r.status };
    } catch (e) {
      URL_PROBE_RESULTS[name] = { ok: false, error: String(e) };
    }
  }
}
// At boot: server.listen(...) → probeUrls() → log results.
// In the renderer: only emit a templated URL when probe is ok.
```

### UI degradation rules

| Probe state | Authoritative `product_url` available | Render as |
|---|---|---|
| ok          | yes | clickable `product →` |
| ok          | no  | clickable `search →` (templated fallback) |
| failed      | yes | clickable `product →` (templated fallback skipped) |
| failed      | no  | **plain `no link` muted text** — no broken click |

Never ship a clickable URL that we know is broken. Better to
render plain text and tell the user *why* than to hand them a
zero-result search.

### Probes are visible in /health

The probe results land in `/health` JSON so an external monitor
can see when a vendor changes their URL format. Surface a status
chip in the page header too (`✓ all 3 vendor URLs probed OK` or
`⚠ 1/3 vendor URLs failed probe`).

### Reference implementation

`aci-provenance/server.mjs` — see `URL_PROBES` + `probeUrls()`
and the `vendorSearchUrl` block in `renderHit` that gates each
templated URL on its probe result.

## Verifiability — every datasheet citation must deep-link to its PDF page

Whenever a tool emits a provenance / citation string that references
a datasheet page (`page 609`, `pages 11-14`, `§5.1.2`, `Figure 167`)
into a UI surface, the page-number mention MUST be rendered as a
clickable link that opens the served PDF at that page (the
standard `#page=N` URL fragment is honored by Chrome / Firefox /
PDF.js — no plugin needed).

**Why it matters.** AI workflows have a documented track record of
inventing plausible-looking page numbers when the underlying
extraction failed. Without a one-click jump to the page itself,
the user can't tell a hallucinated "page 609" from a real one.
User words 2026-04-29: *"the adom user needs to be able to easily
verify what you think is the correct info to make sure you got it
right."*

**How to apply.** Tools that generate UI surfaces over provenance
data (the `aci provenance` Hydrogen webview, the components-library
dashboard, chipfit's own cards) wrap every page-mention in the
provenance JSON in an anchor whose `href` is the local PDF asset
+ `#page=N`. Tools that generate the provenance JSON itself just
include the page number in the citation string in a parseable form
(`page N`, `pages N-M`, `p. N`) so the UI's regex can find it.

The `aci-provenance` MVP at `aci-provenance/server.mjs` shows the
canonical implementation — see `linkifyPages(text, pdfHref)` plus
the `<div class="pdf-banner">` and per-card `<a class="pdf-button">
open PDF →</a>` affordances.

## Deep 3D-model search — try manufacturer-direct before falling back

Don't settle for the kicad-packages3d generic-package model on the
first hop. Real EE's want the chip rendered with the manufacturer's
actual logo + part-number laser-etch on top. Most manufacturers
publish a downloadable STEP these days, so try.

User feedback 2026-04-29: *"as an electrical engineer i'd really
like the real chip that even has the logo emblazened on it
possibly from the manufacturer. like in the case of the rp2040
they have a cute raspberry pi logo on the chip. i also really
like seeing the part number laser etched onto the chip. so could
you search far and wide on the manufacturers website to try to
find an actual real 3d chip that they might be sharing to the
world?"*

### Priority order

1. **Manufacturer website.** Highest fidelity — usually carries
   the chip with real logo + marking. Each manufacturer has its
   own product-page structure ("CAD Resources" tab, "Documentation
   → Package Information", "Tools & Resources"). Recipes lookup
   table:

   | Manufacturer | Where to look |
   |---|---|
   | STMicro      | Product page → CAD Resources → ECAD Models |
   | NXP          | Product page → Documentation → Package Information (some login-gated) |
   | Microchip    | Product page → Tools & Resources → CAD Models |
   | TI           | Product page → Quality & reliability → Package mechanical files |
   | Infineon     | Product page → Models tab |
   | Raspberry Pi | **No public chip-level STEP** as of 2026-04-29 — verified via WebFetch on `raspberrypi.com/products/rp2040/` |

2. **Community curated.** Free with login — sometimes carry
   manufacturer logos / markings:
   - **SnapEDA** — `snapeda.com/search/?q=<MPN>`
   - **Ultra Librarian** — `ultralibrarian.com/search?searchToken=<MPN>`
   - **Component Search Engine (samacsys)** — broadest coverage
   - **GrabCAD** — community-uploaded, quality varies

3. **JLCPCB / LCSC / EasyEDA — last-resort fallback.** ⚠️ Caveat
   from the user 2026-04-29: *"jlcpcb tends to have 3d chips
   available. i hate using theirs because they embed this ugly
   lcsc easyeda logo on their chips, but it is a possible 3d chip
   variant as a fallback."* Acceptable when the user is desperate;
   prefer option 4 over this in nearly every case.

4. **kicad-packages3d generic + chipfit auto-laser-etch.** This is
   the recommended default when the manufacturer doesn't publish
   one. The generic STEP gets the chip's pose right; chipfit's
   bake-time `addLaserEtch` (chipfit/PLAN.md Part 6) then draws
   the manufacturer logo + part-number marking on the top face.
   Beats the JLCPCB/EasyEDA-logo render every time. User: *"i'd
   rather use the plain 3d chip from kicad in cases like that and
   then have you customize during the chipfit bake process to put
   a logo on it and part number."*

### The honest-failure rule

When a manufacturer-direct search fails, the provenance MUST
record:

- **What we tried** — every URL hit + result, so the user can
  retrace our steps.
- **The conclusion** — a single-line statement of what is and
  isn't available.
- **The user hint** — what to do next, including login-gated
  portals we couldn't reach (e.g. "Make a free NXP account for
  the highest-fidelity STEP files").

Without this trace the user can't tell "AI didn't try hard
enough" from "manufacturer genuinely doesn't publish one." The
trace must be honest — never invent searches we didn't run.

### How to apply

The `aci-provenance` MVP at `aci-provenance/server.mjs` shows the
canonical implementation — see `deep3DSearchPlan(mpn,
manufacturer)` and the rendered "Deep 3D model search" card.
Recipes for unrecognised manufacturers fall back to the four
community sources + the JLCPCB last-resort with the
EasyEDA-logo warning.

When you ship a new chip via `aci component <mpn>` and the
manufacturer-direct search returns nothing, write the search
trace into `<mpn>-provenance.json` under `glb.manufacturer_3d_search`
so the next run / a downstream tool / the human reviewer can pick
up where we left off without re-doing the legwork.

## No local KiCad — every KiCad asset goes through service-kicad

Adom users have no local KiCad install on their Docker. The
`kicad-cli` binary, the kicad-symbols / kicad-footprints /
kicad-packages3d libraries, and the demo files all live in the
shared **service-kicad** container — see
[gallia/skills/service-kicad/SKILL.md](../gallia/skills/service-kicad/SKILL.md).
Every Adom tool (chipfit, aci, footprint-creator,
3d-component-creator, etc.) hits the same HTTP surface instead
of apt-installing KiCad locally.

User feedback 2026-04-29: *"is that local on my docker or is
that what's on service-kicad? cuz i don't want to have to
install kicad on every user's docker, that's the whole reason
we made the service-kicad and if its installed on my docker, i
need you to uninstall it so we can operate like a normal adom
user would so you don't get confused and take shortcuts."*

### What this means in practice

- **Never read from `/usr/share/kicad/<anything>` in code** — it
  works in dev (we've often had KiCad apt-installed) but doesn't
  exist on a real Adom user's Docker.
- **Never invoke `/usr/bin/kicad-cli` directly** — go through
  `service-kicad`'s HTTP API or the `service-kicad` CLI wrapper.
- **Provenance / display surfaces** that received a local KiCad
  path from an upstream (older `aci 3d ingest`) tool should
  rewrite the path to the equivalent `service-kicad` URL at
  render time so the user-visible string tells the truth. The
  `aci-provenance` MVP shows this rewrite — see the regex pass
  in `linkifyPages` translating `/usr/share/kicad/<kind>/<rest>`
  into `https://service-kicad-…adom.cloud/models/<kind>/<rest>`.
- **Don't blindly `apt remove kicad` mid-session** to "force the
  issue" — there are still a handful of code paths with hard-
  coded local fallbacks (chipfit's fp-variant glob, aci-kicad's
  test scaffolding). Audit + migrate those FIRST.

### Audit checklist

Before claiming a tool is service-kicad-clean, grep its source
for the offending patterns and replace each one:

```bash
grep -rE "/usr/share/kicad|/usr/bin/kicad-cli|kicad-cli" src/
```

Each match is one of:

1. **Test scaffolding** (parity harnesses, demo file iteration) —
   acceptable to keep as a `cfg(test)`-gated local fallback, but
   document it.
2. **Library asset lookup** (footprint / symbol / 3D-model
   fetch) — must go through `service-kicad`'s `/models/...` /
   `/sym/...` / `/fp/...` endpoints or the `service-kicad model
   fetch` CLI subcommand.
3. **kicad-cli invocation** (DRC, ERC, exports, SVG render) —
   POST to the service-kicad endpoint listed in
   `docs/service-kicad-api.md`.

## File-path affordance — every absolute path is a click-to-reveal

Whenever a UI surface displays an absolute filesystem path
(`/home/adom/project/...`, `/tmp/...`, etc.) it MUST be a
**clickable button that POSTs to your app's `/api/reveal`
endpoint, which shells `adom-vscode reveal <path>`**. Per
[`gallia/skills/human-ui-patterns/SKILL.md` §5b](../gallia/skills/human-ui-patterns/SKILL.md).

⚠️ **Do NOT use the `vscode://file/<path>` URL scheme** — that
opens the editor with the file. The user wants the **Explorer
sidebar** to highlight the file (so they can see neighbouring
artifacts and decide what to do next). Different verb, different
tool.

**Why it matters.** Engineers regularly need to drop into the
real file behind an artifact mention — *"is this the right STEP
file?"*, *"did the symbol creator actually write this?"*, *"what's
in this metadata.json?"*. A flat string forces them to copy-
paste, alt-tab to a terminal, and `code path/to/thing` manually.
The reveal removes 4 steps and surfaces the file in the explorer
sidebar where they can see neighbouring artifacts at a glance.

User feedback 2026-04-29: *"anytime you are showing file paths,
make them clickable to reveal in the adom-vscode file explorer
sidebar."* And later: *"for the reveal button, did you even bother
to look at the adom-vscode skill?"* — the URL-scheme shortcut
above is the failure mode this rule exists to prevent.

**How to apply.** Two-step:

1. Server: a `POST /api/reveal` endpoint that shells
   `adom-vscode reveal <path>`. **Workspace-boundary precheck**
   (per the skill's footgun §): paths outside `$HOME/project`
   silently no-op — fail loudly to the UI instead of pretending
   it worked.

   ```js
   // node example
   const real = require('node:fs').realpathSync(path);
   const wsRoot = `${process.env.HOME}/project`;
   if (!real.startsWith(wsRoot)) {
     return { ok: false,
              error: 'outside the VS Code workspace; ' +
                     'adom-vscode reveal silently no-ops on out-of-workspace paths.' };
   }
   require('node:child_process').execFile(
     'adom-vscode', ['reveal', real], { timeout: 10_000 }, …);
   ```

2. Client: render every path as a `<button>` (NOT an anchor — there's
   nothing to navigate to) with a `data-reveal="<path>"`
   attribute. A document-level click delegate POSTs to
   `/api/reveal` and surfaces a toast (per
   [human-ui-patterns §6](../gallia/skills/human-ui-patterns/SKILL.md#6-feedback-for-every-action)).

   ```js
   document.addEventListener('click', async (ev) => {
     const el = ev.target.closest('[data-reveal]');
     if (!el) return;
     ev.preventDefault();
     const path = el.getAttribute('data-reveal');
     const r = await fetch('api/reveal', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({ path }),
     });
     const j = await r.json();
     showToast(j.ok ? 'revealed · ' + path : 'reveal failed: ' + j.error,
               j.ok ? 'ok' : 'error');
   });
   ```

The `aci-provenance` MVP at `aci-provenance/server.mjs` is the
reference implementation — search for `data-reveal` and
`/api/reveal`. The chipfit composite GLB / STEP / kicad_mod paths
in the provenance trace use this same delegate.

## Multi-option PDF opens — give the user three click targets

When a UI surface offers a "open the datasheet at page N" link,
default-clicking it must land the user at that page. The naïve
`<a href="datasheet.pdf#page=N">` works in browsers using the
internal PDF viewer, but **fails when the browser is configured
to "always download PDFs"** — Acrobat / Preview don't honor URL
fragments, so the user lands at page 1 and has to find their way.

User feedback 2026-04-29: *"when i click on the pdf link, you
download the pdf to my desktop and it opens in acrobat. now, i
like that as an option, but what happens is you don't jump me to
the page then. so is there a better way to open this so you can
link to the page? i think we have to give the user multiple
options on how to open the pdf."*

**The pattern.** Render every page citation as a chip group with
THREE click targets:

| Affordance | Goes to | When you'd want it |
|---|---|---|
| Text link "page N" | `/pdf-viewer?src=…&page=N` — an HTML wrapper that embeds the PDF in an iframe at #page=N. The iframe always uses the browser's internal viewer regardless of download preference, so the page jump survives. | Default. Everyone. |
| `↗` icon | `<pdf>#page=N` opened in a new tab — uses the user's browser-level PDF handling. Page jump survives if they have a PDF.js plugin / browser inline viewer; lost if "always download" is set. | Users who want PDF.js / Acrobat plug-in features (search, annotation tools). |
| `⬇` icon | `<pdf>?download=1` — server sends `Content-Disposition: attachment`. | Users who want to keep a copy / hand off to a desktop reviewer. |

The in-page viewer page should at minimum offer:
- Previous / next page buttons
- A page-number input
- Direct "open in new tab" / "download" buttons in the topbar
- An iframe that re-srcs on page change

## Vendor-neutral copy — don't shove the EDA tool in the user's face

Adom users may be on Fusion 360, Altium, OrCAD, or any of a half-
dozen other EDA tools. The fact that under the hood we generate
artifacts with KiCad CLI services is an implementation detail. UI
text that keeps saying "the KiCad symbol", "via KiCad",
"kicad-cli", "the kicad_mod file" makes Adom feel like a
KiCad-only tool — and bothers the 50%+ of pro EE's who don't use
KiCad day-to-day.

**Why it matters.** User feedback 2026-04-29: *"can you downplay
how much you say kicad, cuz for adom users who use fusion 360 or
altium or orcad they will get bothered by this. i realize
under-the-hood we're using kicad, but we don't have to keep
shoving that in their face."*

**How to apply.**

| User-facing text says… | Display as… |
|---|---|
| "KiCad symbol"          | "symbol" / "symbol diagram" |
| "KiCad footprint"       | "footprint" / "footprint pad layout" |
| "KiCad land pattern"    | "land pattern" |
| "KiCad library footprint" | "library footprint" |
| "Generated KiCad land pattern via footprint-creator skill" | "Generated the land pattern via footprint-creator skill" |
| "kicad-cli"             | "service" / "the EDA service" |
| "kicad-packages3d"      | "CAD library" |
| `.kicad_mod`            | "footprint file" (or just the filename when relevant) |

This is a **display-only** filter. Don't rewrite the on-disk
provenance JSON / scripts / PLAN files — those are engineering
docs and KiCad is the correct technical name there. The aim is
to keep the user's eye on what they're trying to verify, not on
which CAD ecosystem we happen to shell to.

The `aci-provenance` MVP shows the pattern — see `softenImpl(text)`
in `aci-provenance/server.mjs`, applied at render time inside
`linkifyPages` so every value before display passes through the
softener.

## Related

- `adom-parts-search` — source and price the chip you're about to
  validate.
- `adom-molecule --step` — if you only have a STEP file, convert to
  GLB first.
- `electrical-engineering` skill §8 — the underlying EE conventions
  (z=0 at top copper, bake pin-1, family aliases).
- `solder-jetting` skill — the dispense process the
  `chipfit-solder-jets` layer visualises.
