← Commit history
README.mdadded+42
@@ -0,0 +1,42 @@+# Alternate Part++Which part can replace this one on my board, and can I buy it today?++Most parts come in many variants, only a few are in stock at any moment, and which few depends on the distributor. A+same-footprint swap is usually fine, but a cautious agent refuses it "because of a pin function you don't use", and a+careless one approves a part that swaps two pins the board does use. This skill asks the right question: does the+replacement give every pin function **this design uses**, on the same pins, and is it in stock?++## How it works++1. It reads the chip's pads from your board. A pin is used when its net reaches another part.+2. It sends those pins (and their net names) to Component Ocean's `POST /api/alternates`.+3. Component Ocean answers in one of four tiers, and says which and how sure it is:++| tier | parts | checked against | confidence |+|---|---|---|---|+| 1 | STM32 MCUs | ST's own open pin data, every pin and function | high |+| 2 | any part with an Adom component page (about 98,000) | the pages' datasheet-verified pin tables and footprints | medium-high |+| 3 | passives, MOSFETs, diodes, transistors, LDOs, 74-series logic | catalog parameters; pin-order traps flagged | medium, lower where the pin order is unchecked |+| 4 | everything else | nothing verified: the skill walks the AI through checking candidates from their datasheets | stated with the answer |++4. Candidates come back ranked: in stock now first, then no downgrade, then the closest spec. Every stock number says+   where it came from: live (at most an hour old), cached (older, with its age) or the JLCPCB catalog snapshot (with+   its date).++```+python3 ~/.claude/skills/alternate-part/scripts/alternates.py board.kicad_pcb --ref U1+python3 ~/.claude/skills/alternate-part/scripts/alternates.py --mpn LM358DR+```++Example, the STM32F411RET6 on an Adom MCU board: 29 of its 64 pins are used; the parts that fit come back with the+pins that block each rejected one. An LM358DR gets tier 2: TL082CDT, LM258DT and MC33178DR2G among its same-pinout+SOIC-8 dual op-amps.++A good candidate with no Adom page can be requested (`POST /api/part-request`); once built, it has tier 2 data for+everyone.++## Install++`adom-wiki pkg install adom/alternate-part`, then ask Claude "this chip is out of stock, what can replace it on my+board?"
docs/hero.pngadded
⋯ 1 unchanged line ⋯
install.shadded+4
@@ -0,0 +1,4 @@+#!/bin/sh+set -eu+SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)+exec python3 "$SCRIPT_DIR/scripts/install.py" "$@"
package.jsonadded+80
@@ -0,0 +1,80 @@+{+  "name": "alternate-part",+  "slug": "alternate-part",+  "title": "Alternate Part",+  "version": "0.2.0",+  "type": "skill",+  "org": "adom",+  "description": "Find a part that can replace one on THIS board, in stock now, for any part: it reads the design, works out which pins it uses, and ranks same-package candidates by stock. Every answer says its tier and how sure it is: STM32 from ST's own pin data, any part with an Adom page from its datasheet-verified pin table, passives and simple parts by parameters, and for everything else a step-by-step check from the datasheets.",+  "brief": "Which part can replace this one on my board, in stock now: any part, with the answer saying how sure it is.",+  "license": "CC-BY-SA-4.0",+  "files": [+    "package.json",+    "README.md",+    "skills/**",+    "install.sh",+    "uninstall.sh",+    "scripts/install.py",+    "scripts/uninstall.py",+    "docs/hero.png",+    "screenshots/showcase.png"+  ],+  "dependencies": {},+  "tags": [+    "parts",+    "alternates",+    "stock",+    "kicad",+    "claude",+    "alternate",+    "replacement",+    "drop-in",+    "out of stock"+  ],+  "author": {+    "name": "Ray",+    "email": "[email protected]"+  },+  "discovery_triggers": [+    "replacement chip",+    "alternate part",+    "drop-in replacement",+    "out of stock substitute",+    "pin compatible",+    "what else fits this footprint",+    "second source",+    "what can replace my STM32",+    "which STM32 is in stock",+    "swap the MCU",+    "out of stock, what can replace this chip",+    "alternate part for",+    "drop-in replacement for",+    "what can replace this part",+    "equivalent part",+    "cross reference part",+    "substitute part",+    "replacement for an out of stock part"+  ],+  "discovery_pitch": "Use when a part is out of stock or someone asks for an alternate, for any part: it reads the design's used pins and functions and returns in-stock parts that provide all of them on the same pins, ranked, each answer saying its tier (STM32 pin data, Adom pages, parameters, or a datasheet method) and how sure it is.",+  "sample_prompts": [+    {+      "label": "Replace my MCU",+      "prompt": "what can replace the STM32 on my board that's actually in stock?"+    },+    {+      "label": "One part",+      "prompt": "find an in-stock drop-in for STM32U5A5VJT6 in my design"+    }+  ],+  "visibility": "public",+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "hero": {+    "type": "image",+    "path": "docs/hero.png",+    "headline": "Alternate Part",+    "subhead": "Which chip can replace this one on my board, in stock now."+  }+}
page.jsonadded+80
@@ -0,0 +1,80 @@+{+  "name": "alternate-part",+  "slug": "alternate-part",+  "title": "Alternate Part",+  "version": "0.2.0",+  "type": "skill",+  "org": "adom",+  "description": "Find a part that can replace one on THIS board, in stock now, for any part: it reads the design, works out which pins it uses, and ranks same-package candidates by stock. Every answer says its tier and how sure it is: STM32 from ST's own pin data, any part with an Adom page from its datasheet-verified pin table, passives and simple parts by parameters, and for everything else a step-by-step check from the datasheets.",+  "brief": "Which part can replace this one on my board, in stock now: any part, with the answer saying how sure it is.",+  "license": "CC-BY-SA-4.0",+  "files": [+    "package.json",+    "README.md",+    "skills/**",+    "install.sh",+    "uninstall.sh",+    "scripts/install.py",+    "scripts/uninstall.py",+    "docs/hero.png",+    "screenshots/showcase.png"+  ],+  "dependencies": {},+  "tags": [+    "parts",+    "alternates",+    "stock",+    "kicad",+    "claude",+    "alternate",+    "replacement",+    "drop-in",+    "out of stock"+  ],+  "author": {+    "name": "Ray",+    "email": "[email protected]"+  },+  "discovery_triggers": [+    "replacement chip",+    "alternate part",+    "drop-in replacement",+    "out of stock substitute",+    "pin compatible",+    "what else fits this footprint",+    "second source",+    "what can replace my STM32",+    "which STM32 is in stock",+    "swap the MCU",+    "out of stock, what can replace this chip",+    "alternate part for",+    "drop-in replacement for",+    "what can replace this part",+    "equivalent part",+    "cross reference part",+    "substitute part",+    "replacement for an out of stock part"+  ],+  "discovery_pitch": "Use when a part is out of stock or someone asks for an alternate, for any part: it reads the design's used pins and functions and returns in-stock parts that provide all of them on the same pins, ranked, each answer saying its tier (STM32 pin data, Adom pages, parameters, or a datasheet method) and how sure it is.",+  "sample_prompts": [+    {+      "label": "Replace my MCU",+      "prompt": "what can replace the STM32 on my board that's actually in stock?"+    },+    {+      "label": "One part",+      "prompt": "find an in-stock drop-in for STM32U5A5VJT6 in my design"+    }+  ],+  "visibility": "public",+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "hero": {+    "type": "image",+    "path": "docs/hero.png",+    "headline": "Alternate Part",+    "subhead": "Which chip can replace this one on my board, in stock now."+  }+}\ No newline at end of file
scripts/install.pyadded+15
@@ -0,0 +1,15 @@+#!/usr/bin/env python3+"""Install only this package's skills into Claude and Codex homes."""+import argparse, json, os, shutil+from pathlib import Path+ROOT=Path(__file__).resolve().parents[1]+parser=argparse.ArgumentParser();parser.add_argument('--home',type=Path,help='Isolated home for validation');args=parser.parse_args()+home=args.home or Path.home()+codex=home/'.codex' if args.home else Path(os.environ.get('CODEX_HOME',str(home/'.codex')))+for destination in [home/'.claude/skills',codex/'skills']:+    for source in sorted((ROOT/'skills').iterdir()):+        if not (source/'SKILL.md').is_file():continue+        target=destination/source.name+        target.mkdir(parents=True,exist_ok=True)+        shutil.copytree(source,target,dirs_exist_ok=True)+        print(target)
scripts/uninstall.pyadded+16
@@ -0,0 +1,16 @@+#!/usr/bin/env python3+"""Remove only unchanged files installed by this package; preserve local edits."""+import argparse,os+from pathlib import Path+root=Path(__file__).resolve().parents[1]+p=argparse.ArgumentParser();p.add_argument('--home',type=Path);args=p.parse_args()+home=args.home or Path.home();codex=home/'.codex' if args.home else Path(os.environ.get('CODEX_HOME',str(home/'.codex')))+for base in [home/'.claude/skills',codex/'skills']:+ for source in sorted((root/'skills').iterdir()):+  target=base/source.name+  for file in source.rglob('*'):+   out=target/file.relative_to(source)+   if file.is_file() and out.is_file() and out.read_bytes()==file.read_bytes():out.unlink()+  if target.exists():+   for d in sorted([p for p in target.rglob('*') if p.is_dir()]+[target],key=lambda p:len(p.parts),reverse=True):+    if not any(d.iterdir()):d.rmdir()
skills/alternate-part/SKILL.mdadded+86
@@ -0,0 +1,86 @@+---+name: alternate-part+description: Find a part that can replace one on THIS board, in stock now, for any part. Reads the user's design, works out which pins it actually uses and for what, and asks Component Ocean for candidates in the same package, ranked by stock. Every answer says which tier it came from and how sure it is - tier 1 STM32 from ST's own pin data, tier 2 any part with an Adom component page (datasheet-verified pin tables), tier 3 passives, discretes, LDOs and logic by parameters, tier 4 everything else, where the skill walks you through checking candidates from their datasheets yourself. Use when a part is out of stock, when someone asks for an alternate, substitute, second source or drop-in replacement, or "what else fits this footprint". Trigger words - out of stock, what can replace this chip, alternate part, alternative part, drop-in replacement, substitute, second source, pin compatible, what else fits this footprint, replacement for, swap the MCU, equivalent part, cross reference, which STM32 is in stock.+---++# Alternate part++The question is never "are these two parts identical". It is "does the replacement give every pin function THIS+design uses, on the same pins, within the limits the design needs, and can I buy it today". A part that lacks a+function the board never uses is a fine replacement; refusing it for that reason is the mistake this skill exists to+avoid. A part that fits the footprint but swaps two pins the board uses is the mistake that costs a board: never call+something a drop-in that was not checked pin by pin.++## Read this first: the four tiers++Every answer from Component Ocean carries `tier`, `confidence` and `basis`. They tell you how much to trust it.++| tier | what it covers | where the pins come from | trust it? |+|---|---|---|---|+| 1 | STM32 MCUs | ST's open pin data, every pin and alternate function | yes: high confidence, the manufacturer's own table |+| 2 | any part with an Adom component page | the page's datasheet-verified pin table and footprint, compared with candidate pages | mostly: medium-high; check the `warnings` and `fold_notes` |+| 3 | resistors, capacitors, inductors, beads, MOSFETs, diodes, transistors, LDOs, 74-series logic | catalog parameters (value, tolerance, ratings, package, dielectric) | for passives, yes after reading `warnings`; for 3-pin parts the pin ORDER is the trap (SOT-23 MOSFET G-S-D vs G-D-S, LDO pinouts): believe it only where the answer says the order was checked from pages, else check the datasheet |+| 4 | everything else | nothing verified | no claimed answer: the response gives the `method`, and you do the work (below) |++Tier 2 covers every part with an Adom page (`adom_pages.indexed` in the response says how many: about 98,000 on+2026-10-30). STM32 is the best supported; everything else is real but less certain, and the answer says so.++## The steps++1. **Find the part and the design.** Ask which part, if the user did not say. Get the board:+   - a KiCad board (`.kicad_pcb`) in the workspace: use it directly;+   - KiCad, Altium or Fusion open on the user's PC: export the board or a netlist through the bridge, or ask for the file;+   - no design: ask which pins and functions they use, and pass them with `--used`.+2. **Run the check.** It reads the chip's pads, counts a pin as used when its net reaches another pad, and makes one+   call to Component Ocean:++   ```+   python3 ~/.claude/skills/alternate-part/scripts/alternates.py board.kicad_pcb --ref U1 [--qty 10]+   python3 ~/.claude/skills/alternate-part/scripts/alternates.py --mpn STM32U5A5VJT6 --used PA9:USART1_TX,PA10:USART1_RX+   python3 ~/.claude/skills/alternate-part/scripts/alternates.py --mpn LM358DR+   ```++   `--json` gives the full answer. A net named after a pin's function (`USART1_TX` on PA9) makes that function+   required; any other net name only marks the pin used.+3. **Read the answer by its tier.** The first line says tier and confidence. Then each candidate:+   - `grade`: `drop-in for this design` (every used pin matches and no supply, ground or reset pin moves),+     `drop-in`, `drop-in except listed pins`, `equivalent` (tier 3, by parameters), `not compatible` (in `rejected`:+     never offer these);+   - `stock`: per distributor, with its source. `live` is a distributor answer at most an hour old; `cached` is an+     older one (the refresh failed: say how old); `snapshot` is the JLCPCB catalog, with its date. Rank by live+     stock; never present a cached or snapshot number as "in stock now" without saying its age;+   - `warnings`: read every one aloud to the user (a different test condition, an unknown rating, an unchecked pin+     order).+4. **Answer plainly.** Lead with the tier and how sure it is, then the best in-stock candidates with where they are in+   stock, how many, and what changes. Then the ones that fit but are out of stock.++## Tier 4, or a tier 2-3 answer with no candidates: do the work yourself++The response gives `method`. Follow it, and say in the answer that this was read from datasheets, not verified data:++1. Find same-package candidates: Component Ocean search (`GET /api/search`, `GET /api/part-candidates`) and the+   distributors, same package and pin count.+2. Fetch each candidate's manufacturer datasheet (`GET /api/part/<mpn>` gives `datasheet_urls`, the manufacturer's+   copy first).+3. Extract the pin table of the exact package from each datasheet.+4. Compare only the pins and functions this design uses: same pin number, same function; supply, ground, reset and+   enable pins must not move.+5. Check the electrical limits the design depends on: supply range, current, switching frequency or clock,+   thresholds, temperature range.+6. Check stock (`POST /api/enrich`) and rank: in stock now first, then no downgrade, then the closest spec.+7. Report a confidence level and why.++When a good candidate has no Adom page, offer to request one: `POST /api/part-request {mpn}` (the part-request skill+covers it). Once built, that part has tier 2 data for everyone next time.++## When nothing fits++Say so, then the real options, without selling them: a part that fits except for named pins (the board needs those+traces moved, a respin); a different package (a new footprint, a respin); the same part at another distributor, or+waiting for stock. Never invent a pin function, a rating or a stock count; "Unknown" when no source gives one.++## Install and test++`adom-wiki pkg install adom/alternate-part` copies this skill into `~/.claude/skills/`. Test the installed copy with+`python3 ~/.claude/skills/alternate-part/tests/run.py` (standard library only, no network: the tier cases are saved+real answers). The comparison itself is tested in Component Ocean (`src/alternates.test.ts`).
skills/alternate-part/scripts/__pycache__/alternates.cpython-312.pycadded
⋯ 1 unchanged line ⋯
skills/alternate-part/scripts/alternates.pyadded+145
@@ -0,0 +1,145 @@+#!/usr/bin/env python3+"""Which parts can replace a chip on THIS board, in stock now (Component Ocean's POST /api/alternates).++    alternates.py <board.kicad_pcb> [--ref U1 | --mpn STM32F411RET6] [--qty 10] [--allow-downgrade] [--json]+    alternates.py --mpn STM32U5A5VJT6 [--used PA9:USART1_TX,PA10:USART1_RX]       # no board: say which pins matter++From a board, a pin counts as USED when its net reaches any other pad on the board; a pad alone on its net (or on an+"unconnected-" net) is unused. Each used pin is sent with its net name, and Component Ocean turns a net named after one+of the pin's functions (USART1_TX on PA9) into a required function. Supply, ground and reset pins always count.+Standard library only; one HTTP call; no model.+"""+import argparse, collections, json, os, re, sys, urllib.request++CO = os.environ.get('COMPONENT_OCEAN_URL', 'https://component-ocean-jk89eeqplwfl.adom.cloud')+++def sexp_blocks(text, head):+    """Every balanced '(head ...' block in a KiCad s-expression file."""+    out, i = [], 0+    while True:+        i = text.find('(' + head, i)+        if i < 0: return out+        depth, j = 0, i+        while j < len(text):+            c = text[j]+            if c == '"':+                j = text.index('"', j + 1)+                while text[j - 1] == '\\': j = text.index('"', j + 1)+            elif c == '(': depth += 1+            elif c == ')':+                depth -= 1+                if depth == 0: break+            j += 1+        out.append(text[i:j + 1]); i = j + 1+++def board_pins(path, ref=None, mpn=None):+    """(mpn, ref, [{pin, net}] used, n_pads) for the chip named by ref or by its value (MPN)."""+    text = open(path, encoding='utf-8', errors='replace').read()+    fps = sexp_blocks(text, 'footprint ')+    def prop(fp, k):+        m = re.search(r'\(property\s+"' + k + r'"\s+"([^"]*)"', fp) or re.search(r'\(fp_text\s+' + k.lower() + r'\s+"?([^"\s)]+)', fp)+        return m.group(1) if m else None+    pick = [f for f in fps if (ref and prop(f, 'Reference') == ref) or (mpn and (prop(f, 'Value') or '').upper() == mpn.upper())]+    if not pick:+        sys.exit(f'no footprint with {"reference " + ref if ref else "value " + str(mpn)} in {path}')+    fp = pick[0]+    def pad_nets(f):+        for pad in sexp_blocks(f, 'pad '):+            num = re.match(r'\(pad\s+"([^"]*)"', pad)+            net = re.search(r'\(net\s+(?:\d+\s+)?"([^"]*)"\)', pad)+            yield (num.group(1) if num else ''), (net.group(1) if net else '')+    count = collections.Counter(net for f in fps for _, net in pad_nets(f) if net)+    used, pads = [], 0+    for pin, net in pad_nets(fp):+        pads += 1+        if net and not net.lower().startswith('unconnected') and count[net] >= 2:+            used.append({'pin': pin, 'net': net})+    return (prop(fp, 'Value') or mpn), prop(fp, 'Reference'), used, pads+++def main():+    ap = argparse.ArgumentParser()+    ap.add_argument('board', nargs='?'); ap.add_argument('--ref'); ap.add_argument('--mpn'); ap.add_argument('--used')+    ap.add_argument('--qty', type=int, default=1); ap.add_argument('--allow-downgrade', action='store_true')+    ap.add_argument('--json', action='store_true')+    a = ap.parse_args()+    body = {'qty': a.qty, 'allow_downgrade': a.allow_downgrade}+    if a.board:+        mpn, ref, used, pads = board_pins(a.board, a.ref, a.mpn)+        body.update(mpn=mpn, used_pins=used)+        print(f'{ref} {mpn}: {len(used)} of {pads} pads are on a net with another part', file=sys.stderr)+    else:+        if not a.mpn: sys.exit('give a board, or --mpn')+        body['mpn'] = a.mpn+        if a.used:+            body['used_pins'] = [{'name': x.split(':')[0], 'functions': [f for f in x.split(':')[1:] if f]} for x in a.used.split(',') if x]+    req = urllib.request.Request(f'{CO}/api/alternates', data=json.dumps(body).encode(), method='POST',+                                 headers={'Content-Type': 'application/json', 'User-Agent': 'alternate-part/0.2', 'X-Adom-Actor': 'user:alternate-part'})+    try:+        d = json.load(urllib.request.urlopen(req, timeout=180))+    except urllib.error.HTTPError as e:+        sys.exit(f'Component Ocean: {e.code} {e.read()[:300].decode(errors="replace")}')+    if a.json:+        print(json.dumps(d, indent=1)); return+    print(render(d))+++def _age(ts):+    """'2 h old' from an ISO time or epoch seconds; '' when unknown."""+    import datetime as _dt+    try:+        t = _dt.datetime.fromtimestamp(ts, _dt.timezone.utc) if isinstance(ts, (int, float)) else \+            _dt.datetime.fromisoformat(str(ts).replace('Z', '+00:00'))+    except (ValueError, TypeError, OSError):+        return ''+    h = (_dt.datetime.now(_dt.timezone.utc) - t).total_seconds() / 3600+    return f'{h * 60:.0f} min old' if h < 1 else f'{h:.0f} h old' if h < 48 else f'{h / 24:.0f} days old'+++def stock_line(st):+    """'Mouser 9942 (live, 12 min old), JLCPCB 973068 (snapshot 2026-04-01)': where every number came from."""+    out = []+    for name, key in (('Mouser', 'mouser'), ('DigiKey', 'digikey'), ('JLCPCB', 'jlcpcb')):+        v = st.get(key) if isinstance(st, dict) else None+        if not v or v.get('stock') is None: continue+        src = v.get('source') or '?'+        when = f"snapshot {v.get('snapshot_date')}" if src == 'snapshot' else f"{src}, {_age(v.get('fetched_at'))}"+        out.append(f"{name} {v['stock']} ({when.rstrip(', ')})")+    return ', '.join(out) or 'no stock data'+++def render(d):+    """The plain-text answer: the tier and how sure it is first, then the candidates, then what was rejected."""+    tier, conf = d.get('tier'), d.get('confidence')+    lines = [f"{d.get('mpn')} ({d.get('ref') or '-'}, {d.get('package') or 'package unknown'}): tier {tier}, confidence {conf}",+             f"  basis: {d.get('basis', '')}"]+    if tier == 4 or (not d.get('candidates') and d.get('method')):+        lines.append('  No verified candidates. Do the check yourself, the way the method says:')+        m = d.get('method') or []+        lines += [f'    {i + 1}. {s}' for i, s in enumerate(m if isinstance(m, list) else [m])]+        for c in (d.get('candidates_unverified') or d.get('unverified') or [])[:8]:+            lines.append(f"    unverified same-package candidate: {c.get('mpn') if isinstance(c, dict) else c}")+    cands = d.get('candidates') or []+    if cands:+        lines.append(f"  {len(cands)} candidates" + (f", {d.get('rejected_total')} rejected" if d.get('rejected_total') else '')+                     + (f", {d['downgrades_hidden']} downgrades hidden (--allow-downgrade shows them)" if d.get('downgrades_hidden') else ''))+    for c in cands[:10]:+        live = c.get('in_stock_live')+        flag = 'IN STOCK ' if live else ('in stock?' if c.get('in_stock') else 'out      ')+        dl = c.get('deltas') or {}+        delta = f" flash {dl['flash_kb']:+g} KB, RAM {dl['ram_kb']:+g} KB" if dl.get('flash_kb') is not None and dl.get('ram_kb') is not None else ''+        who = f"{c.get('mpn', '')} ({c['manufacturer']})" if c.get('manufacturer') else c.get('mpn', '')+        lines.append(f"  {flag} {who:<34} {c.get('grade', ''):<26} tier {c.get('tier', tier)}, {c.get('confidence', conf)}{delta}")+        lines.append(f"            stock: {stock_line(c.get('stock') or {})}")+        for w in (c.get('warnings') or [])[:2]: lines.append(f'            warning: {w}')+        if c.get('differences_unused'): lines.append(f"            unused pins differ: {'; '.join(c['differences_unused'][:4])}")+    for r in (d.get('rejected') or [])[:3]:+        lines.append(f"  rejected {r.get('mpn')}: {'; '.join((r.get('blockers') or [])[:2])}")+    if d.get('stock_freshness', {}).get('note'):+        lines.append(f"  stock sources: {d['stock_freshness']['note']}")+    return '\n'.join(lines)++if __name__ == '__main__':+    main()
skills/alternate-part/tests/fixtures/2N7002.jsonadded+1
@@ -0,0 +1 @@+{"mpn": "2N7002", "tier": 3, "confidence": "low-medium", "basis": "catalog parameters (value, tolerance, ratings, package) from Component Ocean's parametric data; pin order only where Adom pages give it", "ref": null, "package": "SOT-23", "manufacturer": "Changjiang Electronics Tech (CJ)", "category": "MOSFETs", "group": "mosfet", "specs": {"channel": "1 N-channel", "vds": "60V", "id": "115mA", "rds_on": "5\u03a9@10V"}, "rules": ["package: same", "channel: same", "vds: equal or higher", "id: equal or higher", "rds_on: equal or lower"], "pinout_trap": "SOT-23 MOSFETs are mostly 1=G 2=S 3=D, but some makers print 1=G 2=D 3=S (DMG3414U-HXY on its Adom page): check the pin order", "page_url": "https://wiki.adom.inc/adom/2n7002", "mode": "parametric", "used_pins": null, "qty": 1, "candidates": [{"mpn": "2N7002", "manufacturer": "JSMSEMI", "package": "SOT-23", "lcsc": "C916396", "tier": 3, "confidence": "low-medium", "basis": "catalog parameters (value, tolerance, ratings, package) from Component Ocean's parametric data; pin order only where Adom pages give it", "grade": "equivalent, unconfirmed", "blockers": [], "downgrade": false, "downgrades": [], "warnings": ["rds_on: 1.7\u03a9@5V,50mA vs 5\u03a9@10V, not at the same test condition"], "params": {"channel": {"original": "1 N-channel", "candidate": "1 N-channel"}, "vds": {"original": "60V", "candidate": "60V"}, "id": {"original": "115mA", "candidate": "115mA"}, "rds_on": {"original": "5\u03a9@10V", "candidate": "1.7\u03a9@5V,50mA"}}, "deltas": {"channel": null, "vds": 1, "id": 1, "rds_on": 0.34}, "spec_distance": 0, "same_mpn_other_maker": true, "pinout": "confirmed from Adom pages", "page_url": "https://wiki.adom.inc/adom/2n7002", "stock": {"jlcpcb": {"lcsc": "C916396", "stock": 611303, "as_of": 1733546443, "fetched_at": "2024-12-07T04:40:43.000Z", "source": "snapshot", "snapshot_date": "2026-04-01", "note": "JLCPCB catalog snapshot"}, "mouser": {"stock": 464421, "price_usd": 1.13, "as_of": "2026-10-21T01:34:14.164Z", "fetched_at": "2026-10-21T01:34:14.164Z", "source": "cached"}, "digikey": null}, "in_stock": true, "in_stock_live": false, "stock_total": 1075724, "stock_basis": "snapshot", "fold_notes": [], "mouser_pn": "512-2N7002", "digikey_pn": null}, {"mpn": "2N7002", "manufacturer": "KUU", "package": "SOT-23", "lcsc": "C2944141", "tier": 3, "confidence": "low-medium", "basis": "catalog parameters (value, tolerance, ratings, package) from Component Ocean's parametric data; pin order only where Adom pages give it", "grade": "equivalent, unconfirmed", "blockers": [], "downgrade": false, "downgrades": [], "warnings": ["vds: unknown for the candidate (original 60V)", "rds_on: 3\[email protected] vs 5\u03a9@10V, not at the same test condition"], "params": {"channel": {"original": "1 N-channel", "candidate": "N-Channel"}, "vds": {"original": "60V", "candidate": null}, "id": {"original": "115mA", "candidate": "115mA"}, "rds_on": {"original": "5\u03a9@10V", "candidate": "3\[email protected]"}}, "deltas": {"channel": null, "vds": null, "id": 1, "rds_on": 0.6}, "spec_distance": 0, "same_mpn_other_maker": true, "pinout": "confirmed from Adom pages", "page_url": "https://wiki.adom.inc/adom/2n7002", "stock": {"jlcpcb": {"lcsc": "C2944141", "stock": 26705, "as_of": 1759036187, "fetched_at": "2025-09-28T05:09:47.000Z", "source": "snapshot", "snapshot_date": "2026-04-01", "note": "JLCPCB catalog snapshot"}, "mouser": {"stock": 464421, "price_usd": 1.13, "as_of": "2026-10-21T01:34:14.164Z", "fetched_at": "2026-10-21T01:34:14.164Z", "source": "cached"}, "digikey": null}, "in_stock": true, "in_stock_live": false, "stock_total": 491126, "stock_basis": "snapshot", "fold_notes": [], "mouser_pn": "512-2N7002", "digikey_pn": null}, {"mpn": "2N7002", "manufacturer": "TDSEMIC", "package": "SOT-23", "lcsc": "C42441800", "tier": 3, "confidence": "low-medium", "basis": "catalog parameters (value, tolerance, ratings, package) from Component Ocean's parametric data; pin order only where Adom pages give it", "grade": "equivalent, unconfirmed", "blockers": [], "downgrade": false, "downgrades": [], "warnings": ["vds: unknown for the candidate (original 60V)", "rds_on: unknown for the candidate (original 5\u03a9@10V)"], "params": {"channel": {"original": "1 N-channel", "candidate": "1 N-channel"}, "vds": {"original": "60V", "candidate": null}, "id": {"original": "115mA", "candidate": "115mA"}, "rds_on": {"original": "5\u03a9@10V", "candidate": null}}, "deltas": {"channel": null, "vds": null, "id": 1, "rds_on": null}, "spec_distance": 0, "same_mpn_other_maker": true, "pinout": "confirmed from Adom pages", "page_url": "https://wiki.adom.inc/adom/2n7002", "stock": {"jlcpcb": {"lcsc": "C42441800", "stock": 20419, "as_of": 1746507166, "fetched_at": "2025-05-06T04:52:46.000Z", "source": "snapshot", "snapshot_date": "2026-04-01", "note": "JLCPCB catalog snapshot"}, "mouser": {"stock": 464421, "price_usd": 1.13, "as_of": "2026-10-21T01:34:14.164Z", "fetched_at": "2026-10-21T01:34:14.164Z", "source": "cached"}, "digikey": null}, "in_stock": true, "in_stock_live": false, "stock_total": 484840, "stock_basis": "snapshot", "fold_notes": [], "mouser_pn": "512-2N7002", "digikey_pn": null}, {"mpn": "2N7002", "manufacturer": "Slkor(SLKORMICRO Elec.)", "package": "SOT-23", "lcsc": "C350313", "tier": 3, "confidence": "low-medium", "basis": "catalog parameters (value, tolerance, ratings, package) from Component Ocean's parametric data; pin order only where Adom pages give it", "grade": "equivalent, unconfirmed", "blockers": [], "downgrade": false, "downgrades": [], "warnings": ["rds_on: 7.5\u03a9@5V,50mA vs 5\u03a9@10V, not at the same test condition"], "params": {"channel": {"original": "1 N-channel", "candidate": "1 N-channel"}, "vds": {"original": "60V", "candidate": "60V"}, "id": {"original": "115mA", "candidate": "115mA"}, "rds_on": {"original": "5\u03a9@10V", "candidate": "7.5\u03a9@5V,50mA"}}, "deltas": {"channel": null, "vds": 1, "id": 1, "rds_on": 1.5}, "spec_distance": 0, "same_mpn_other_maker": true, "pinout": "confirmed from Adom pages", "page_url": "https://wiki.adom.inc/adom/2n7002", "stock": {"jlcpcb": {"lcsc": "C350313", "stock": 6793, "as_of": 1733375571, "fetched_at": "2024-12-05T05:12:51.000Z", "source": "snapshot", "snapshot_date": "2026-04-01", "note": "JLCPCB catalog snapshot"}, "mouser": {"stock": 464421, "price_usd": 1.13, "as_of": "2026-10-21T01:34:14.164Z", "fetched_at": "2026-10-21T01:34:14.164Z", "source": "cached"}, "digikey": null}, "in_stock": true, "in_stock_live": false, "stock_total": 471214, "stock_basis": "snapshot", "fold_notes": [], "mouser_pn": "512-2N7002", "digikey_pn": null}], "candidates_total": 402, "rejected": [{"mpn": "AO3400A", "manufacturer": "Alpha & Omega Semicon", "tier": 3, "blockers": ["vds 30V, the original is 60V"]}, {"mpn": "AO3400A", "manufacturer": "UMW(Youtai Semiconductor Co., Ltd.)", "tier": 3, "blockers": ["vds 30V, the original is 60V"]}], "rejected_total": 200, "downgrades_hidden": 884, "not_matching": 3305, "notes": [], "adom_pages": {"indexed": 98445, "listed": 98445, "built_at": "2026-10-29T17:15:59.366Z"}, "stock_freshness": {"live_max_age_s": 3600, "snapshot_date": "2026-04-01", "note": "source live = a distributor answer at most an hour old; cached = an older distributor answer (the refresh failed); snapshot = the JLCPCB catalog snapshot"}, "sources": {"params": "JLCPCB catalog parameters (Component Ocean)", "pins": "Adom component pages, where both parts have one", "stock": "JLCPCB catalog snapshot; Mouser and DigiKey live (at most an hour old) for the first 8"}}\ No newline at end of file
skills/alternate-part/tests/fixtures/LM358DR.jsonadded+1
@@ -0,0 +1 @@+{"mpn": "LM358DR", "tier": 2, "confidence": "medium-high", "basis": "datasheet-verified pin tables on Adom component pages, compared position by position, footprint pads compared", "ref": null, "package": "SOIC-8", "manufacturer": "Texas Instruments", "category": "Operational Amplifier", "function_class": "opamp", "page_url": "https://wiki.adom.inc/adom/lm358dr", "pin_count": 8, "specs": null, "mode": "full-diff", "used_pins": null, "qty": 1, "candidates": [{"mpn": "TL082CDT", "manufacturer": "STMicroelectronics", "package": "SO-8", "page_url": "https://wiki.adom.inc/adom/tl082cdt", "tier": 2, "confidence": "medium-high", "basis": "datasheet-verified pin tables on Adom component pages, compared position by position, footprint pads compared", "grade": "drop-in", "blockers": [], "differences_unused": [], "fold_notes": ["pin 4: V- = VCC-", "pin 8: V+ = VCC+"], "footprint_checked": true, "footprint_notes": [], "warnings": [], "deltas": {}, "spec_distance": 0, "stock": {"jlcpcb": {"lcsc": "C13460", "stock": 973068, "as_of": 1694059148, "fetched_at": "2023-09-07T03:59:08.000Z", "source": "snapshot", "snapshot_date": "2026-04-01", "note": "JLCPCB catalog snapshot"}, "mouser": {"stock": 9942, "price_usd": 0.38, "as_of": "2026-10-25T20:02:10.925Z", "fetched_at": "2026-10-25T20:02:10.925Z", "source": "cached"}, "digikey": {"stock": 13916, "price_usd": 0.38, "as_of": "2026-09-24T00:09:15.270Z", "fetched_at": "2026-09-24T00:09:15.270Z", "source": "cached"}}, "in_stock": true, "in_stock_live": false, "stock_total": 996926, "stock_basis": "snapshot", "mouser_pn": "511-TL082CDT", "digikey_pn": "497-2212-2-ND"}, {"mpn": "LM258DT", "manufacturer": "STMicroelectronics", "package": "SO-8", "page_url": "https://wiki.adom.inc/adom/lm258dt", "tier": 2, "confidence": "medium-high", "basis": "datasheet-verified pin tables on Adom component pages, compared position by position, footprint pads compared", "grade": "drop-in", "blockers": [], "differences_unused": [], "fold_notes": ["pin 4: V- = Vcc-", "pin 8: V+ = Vcc+"], "footprint_checked": true, "footprint_notes": [], "warnings": [], "deltas": {}, "spec_distance": 0, "stock": {"jlcpcb": {"lcsc": "C75472", "stock": 154694, "as_of": 1693800032, "fetched_at": "2023-09-04T04:00:32.000Z", "source": "snapshot", "snapshot_date": "2026-04-01", "note": "JLCPCB catalog snapshot"}, "mouser": {"stock": 3, "price_usd": 0.29, "as_of": "2026-09-24T02:41:29.102Z", "fetched_at": "2026-09-24T02:41:29.102Z", "source": "cached"}, "digikey": {"stock": 41171, "price_usd": 0.29, "as_of": "2026-09-24T02:41:28.658Z", "fetched_at": "2026-09-24T02:41:28.658Z", "source": "cached"}}, "in_stock": true, "in_stock_live": false, "stock_total": 195868, "stock_basis": "snapshot", "mouser_pn": "511-LM258D-TR", "digikey_pn": "497-1553-2-ND"}, {"mpn": "MC33178DR2G", "manufacturer": "onsemi", "package": "SOIC-8", "page_url": "https://wiki.adom.inc/adom/mc33178dr2g", "tier": 2, "confidence": "medium-high", "basis": "datasheet-verified pin tables on Adom component pages, compared position by position, footprint pads compared", "grade": "drop-in", "blockers": [], "differences_unused": [], "fold_notes": ["pin 4: V- = VEE", "pin 8: V+ = VCC"], "footprint_checked": true, "footprint_notes": [], "warnings": [], "deltas": {}, "spec_distance": 0, "stock": {"jlcpcb": {"lcsc": "C39113", "stock": 63518, "as_of": 1694059228, "fetched_at": "2023-09-07T04:00:28.000Z", "source": "snapshot", "snapshot_date": "2026-04-01", "note": "JLCPCB catalog snapshot"}, "mouser": {"stock": 44081, "price_usd": 0.48, "as_of": null, "fetched_at": null, "source": "cached"}, "digikey": {"stock": 49387, "price_usd": 0.5, "as_of": "2026-09-24T02:02:41.888Z", "fetched_at": "2026-09-24T02:02:41.888Z", "source": "cached"}}, "in_stock": true, "in_stock_live": false, "stock_total": 156986, "stock_basis": "snapshot", "mouser_pn": "863-MC33178DR2G", "digikey_pn": "MC33178DR2GOSTR-ND"}, {"mpn": "TL062CDT", "manufacturer": "STMicroelectronics", "package": "SO-8", "page_url": "https://wiki.adom.inc/adom/tl062cdt", "tier": 2, "confidence": "medium-high", "basis": "datasheet-verified pin tables on Adom component pages, compared position by position, footprint pads compared", "grade": "drop-in", "blockers": [], "differences_unused": [], "fold_notes": ["pin 4: V- = VCC-", "pin 8: V+ = VCC+"], "footprint_checked": true, "footprint_notes": [], "warnings": [], "deltas": {}, "spec_distance": 0, "stock": {"jlcpcb": {"lcsc": "C6956", "stock": 124747, "as_of": 1694750076, "fetched_at": "2023-09-15T03:54:36.000Z", "source": "snapshot", "snapshot_date": "2026-04-01", "note": "JLCPCB catalog snapshot"}, "mouser": {"stock": 27399, "price_usd": 0.41, "as_of": "2026-10-25T19:46:31.004Z", "fetched_at": "2026-10-25T19:46:31.004Z", "source": "cached"}, "digikey": {"stock": 4331, "price_usd": 0.4, "as_of": "2026-09-24T00:09:55.046Z", "fetched_at": "2026-09-24T00:09:55.046Z", "source": "cached"}}, "in_stock": true, "in_stock_live": false, "stock_total": 156477, "stock_basis": "snapshot", "mouser_pn": "511-TL062CDT", "digikey_pn": "497-2194-2-ND"}], "candidates_total": 38, "rejected": [{"mpn": "OP07CDR", "page_url": "https://wiki.adom.inc/adom/op07cdr", "tier": 2, "blockers": ["pin 1: OUT1 -> OFFSET N1", "pin 2: IN1- -> IN-", "pin 3: IN1+ -> IN+", "pin 5: IN2+ -> NC", "pin 6: IN2- -> OUT"]}, {"mpn": "MCP6002DRG", "page_url": "https://wiki.adom.inc/adom/mcp6002drg", "tier": 2, "blockers": ["pin 1: OUT1 -> VOUTA", "pin 2: IN1- -> VINA-", "pin 3: IN1+ -> VINA+", "pin 5: IN2+ -> VINB+", "pin 6: IN2- -> VINB-"]}], "rejected_total": 2, "downgrades_hidden": 0, "pages_compared": 40, "notes": ["1459 pages share the package and function class; the 40 with the most catalog stock were compared (page_checks raises it, up to 100)"], "adom_pages": {"indexed": 98445, "listed": 98445, "built_at": "2026-10-29T17:15:59.366Z"}, "stock_freshness": {"live_max_age_s": 3600, "snapshot_date": "2026-04-01", "note": "source live = a distributor answer at most an hour old; cached = an older distributor answer (the refresh failed); snapshot = the JLCPCB catalog snapshot"}, "sources": {"pins": "Adom component pages (page.json component.pins), https://wiki.adom.inc/adom/lm358dr", "footprint": "the pages' .kicad_mod pads", "stock": "JLCPCB catalog snapshot; Mouser and DigiKey live (at most an hour old) for the first 8"}}\ No newline at end of file
skills/alternate-part/tests/fixtures/STM32U5A5VJT6.jsonadded+1
@@ -0,0 +1 @@+{"mpn": "STM32U5A5VJT6", "tier": 1, "confidence": "high", "basis": "ST open pin data (STMicroelectronics/STM32_open_pin_data): the manufacturer's own pin table, compared position by position", "ref": "STM32U5A5VJTx", "package": "LQFP100", "specs": {"flash_kb": 4096, "ram_kb": 2514, "freq_mhz": 160}, "mode": "full-diff", "used_pins": null, "qty": 1, "candidates": [{"mpn": "STM32U5G7VJT6", "ref": "STM32U5G7VJTx", "tier": 1, "confidence": "high", "basis": "ST open pin data (STMicroelectronics/STM32_open_pin_data): the manufacturer's own pin table, compared position by position", "grade": "drop-in", "differences_unused": [], "marketing_status": "Active", "specs": {"flash_kb": 4096, "ram_kb": 3024, "freq_mhz": 160}, "deltas": {"flash_kb": 0, "ram_kb": 510, "freq_mhz": 0}, "stock": {"jlcpcb": null, "mouser": {"stock": 13, "price_usd": 22.36, "as_of": "2026-10-29T17:11:55.377Z", "fetched_at": "2026-10-29T17:11:55.377Z", "source": "cached"}, "digikey": null}, "in_stock": true, "in_stock_live": false, "stock_total": 13, "mouser_pn": "511-STM32U5G7VJT6", "digikey_pn": null}, {"mpn": "STM32U595VJT6", "ref": "STM32U595VJTx", "tier": 1, "confidence": "high", "basis": "ST open pin data (STMicroelectronics/STM32_open_pin_data): the manufacturer's own pin table, compared position by position", "grade": "drop-in", "differences_unused": [], "marketing_status": "Active", "specs": {"flash_kb": 4096, "ram_kb": 2514, "freq_mhz": 160}, "deltas": {"flash_kb": 0, "ram_kb": 0, "freq_mhz": 0}, "stock": {"jlcpcb": null, "mouser": {"stock": 0, "price_usd": 18.87, "as_of": "2026-10-29T17:11:48.377Z", "fetched_at": "2026-10-29T17:11:48.377Z", "source": "cached"}, "digikey": {"stock": 0, "price_usd": 19.99, "as_of": "2026-10-29T17:11:49.406Z", "fetched_at": "2026-10-29T17:11:49.406Z", "source": "cached"}}, "in_stock": false, "in_stock_live": false, "stock_total": 0, "mouser_pn": "511-STM32U595VJT6", "digikey_pn": "497-STM32U595VJT6-ND"}, {"mpn": "STM32U595VJT3", "ref": "STM32U595VJTx", "tier": 1, "confidence": "high", "basis": "ST open pin data (STMicroelectronics/STM32_open_pin_data): the manufacturer's own pin table, compared position by position", "grade": "drop-in", "differences_unused": [], "marketing_status": "Preview", "specs": {"flash_kb": 4096, "ram_kb": 2514, "freq_mhz": 160}, "deltas": {"flash_kb": 0, "ram_kb": 0, "freq_mhz": 0}, "stock": {"jlcpcb": null, "mouser": {"stock": 0, "price_usd": 12.49, "as_of": "2026-10-29T17:11:50.021Z", "fetched_at": "2026-10-29T17:11:50.021Z", "source": "cached"}, "digikey": null}, "in_stock": false, "in_stock_live": false, "stock_total": 0, "mouser_pn": "511-STM32U595VJT3", "digikey_pn": null}, {"mpn": "STM32U599VJT6", "ref": "STM32U599VJTx", "tier": 1, "confidence": "high", "basis": "ST open pin data (STMicroelectronics/STM32_open_pin_data): the manufacturer's own pin table, compared position by position", "grade": "drop-in", "differences_unused": [], "marketing_status": "Active", "specs": {"flash_kb": 4096, "ram_kb": 2514, "freq_mhz": 160}, "deltas": {"flash_kb": 0, "ram_kb": 0, "freq_mhz": 0}, "stock": {"jlcpcb": null, "mouser": {"stock": 0, "price_usd": 17.99, "as_of": "2026-10-29T17:11:51.579Z", "fetched_at": "2026-10-29T17:11:51.579Z", "source": "cached"}, "digikey": {"stock": 0, "price_usd": 21.27, "as_of": "2026-10-29T17:11:53.156Z", "fetched_at": "2026-10-29T17:11:53.156Z", "source": "cached"}}, "in_stock": false, "in_stock_live": false, "stock_total": 0, "mouser_pn": "511-STM32U599VJT6", "digikey_pn": "497-STM32U599VJT6-ND"}], "rejected": [{"mpn": "STM32F071V8T6", "ref": "STM32F071V(8-B)Tx", "blockers": ["pin 10: VSS -> PF9", "pin 11: VDD -> PF10", "pin 12: PH0-OSC_IN (PH0) -> PF0-OSC_IN", "pin 13: PH1-OSC_OUT (PH1) -> PF1-OSC_OUT", "pin 19: VSSA -> PF2"]}, {"mpn": "STM32F071V8T7", "ref": "STM32F071V(8-B)Tx", "blockers": ["pin 10: VSS -> PF9", "pin 11: VDD -> PF10", "pin 12: PH0-OSC_IN (PH0) -> PF0-OSC_IN", "pin 13: PH1-OSC_OUT (PH1) -> PF1-OSC_OUT", "pin 19: VSSA -> PF2"]}], "rejected_total": 351, "downgrades_hidden": 15, "notes": [], "sources": {"pins": "STMicroelectronics/STM32_open_pin_data (BSD-3-Clause), STM32U5A5VJTx.xml", "specs": "ST product list (mcu_families)", "stock": "JLCPCB catalog snapshot; Mouser and DigiKey live (at most an hour old) for the first 8"}, "stock_freshness": {"live_max_age_s": 3600, "snapshot_date": "2026-04-01"}}\ No newline at end of file
skills/alternate-part/tests/fixtures/TPS54202DDCR.jsonadded+1
@@ -0,0 +1 @@+{"mpn": "TPS54202DDCR", "tier": 2, "confidence": "medium-high", "basis": "datasheet-verified pin tables on Adom component pages, compared position by position, footprint pads compared", "ref": null, "package": "SOT-23-6", "manufacturer": "Texas Instruments", "category": "DC-DC Converters", "function_class": "dcdc", "page_url": "https://wiki.adom.inc/adom/tps54202ddcr", "pin_count": 6, "specs": null, "mode": "full-diff", "used_pins": null, "qty": 1, "candidates": [], "candidates_total": 0, "rejected": [{"mpn": "MT3608", "page_url": "https://wiki.adom.inc/ray/mt3608", "tier": 2, "blockers": ["pin 1: GND -> SW", "pin 2: SW -> GND", "pin 3: VIN -> FB", "pin 4: FB -> EN", "pin 5: EN -> VIN"]}, {"mpn": "SY8120IABC", "page_url": "https://wiki.adom.inc/adom/sy8120iabc", "tier": 2, "blockers": ["pin 1: GND -> BS", "pin 2: SW -> GND", "pin 3: VIN -> FB", "pin 4: FB -> EN", "pin 5: EN -> IN"]}], "rejected_total": 40, "downgrades_hidden": 0, "pages_compared": 40, "notes": ["451 pages share the package and function class; the 40 with the most catalog stock were compared (page_checks raises it, up to 100)"], "method": ["List same-package candidates: Component Ocean search (GET /api/search, GET /api/part-candidates) and the distributors (Mouser, DigiKey), same package and pin count.", "Fetch each candidate manufacturer's datasheet (GET /api/part/<mpn> gives datasheet_urls, the manufacturer's copy first).", "Extract the pin table of the package the board uses (pin number, name, function) from each datasheet.", "Compare only the pins and functions this design uses: same pin number, same function; supply, ground and reset pins must not move.", "Check the electrical limits the design depends on (supply range, current, timing, thresholds) against the original.", "Check live stock (POST /api/enrich or GET /api/part/<mpn>) and rank: in stock now first, then no downgrade, then closest spec.", "Report each pick with its confidence, and say it was read from datasheets, not verified data."], "adom_pages": {"indexed": 98445, "listed": 98445, "built_at": "2026-10-29T17:15:59.366Z"}, "stock_freshness": {"live_max_age_s": 3600, "snapshot_date": "2026-04-01", "note": "source live = a distributor answer at most an hour old; cached = an older distributor answer (the refresh failed); snapshot = the JLCPCB catalog snapshot"}, "sources": {"pins": "Adom component pages (page.json component.pins), https://wiki.adom.inc/adom/tps54202ddcr", "footprint": "the pages' .kicad_mod pads", "stock": "JLCPCB catalog snapshot; Mouser and DigiKey live (at most an hour old) for the first 8"}}\ No newline at end of file
skills/alternate-part/tests/run.pyadded+45
@@ -0,0 +1,45 @@+#!/usr/bin/env python3+"""Tests for scripts/alternates.py's board reading (standard library only): python3 tests/run.py"""+import json, os, sys+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'scripts'))+import alternates as A++BOARD = '''(kicad_pcb+  (footprint "Package_QFP:LQFP-64"+    (property "Reference" "U1") (property "Value" "STM32F411RET6")+    (pad "1" smd roundrect (at 0 0) (size 1 0.3) (layers "F.Cu") (roundrect_rratio 0.25) (net "VCC") (uuid "a"))+    (pad "42" smd rect (at 1 0) (size 1 0.3) (layers "F.Cu") (net 7 "/USART1_TX") (uuid "b"))+    (pad "43" smd rect (at 2 0) (size 1 0.3) (layers "F.Cu") (net "Net-(U1-PA10)") (uuid "c"))+    (pad "44" smd rect (at 3 0) (size 1 0.3) (layers "F.Cu") (net "unconnected-(U1-PA11-Pad44)") (uuid "d"))+    (pad "45" smd rect (at 4 0) (size 1 0.3) (layers "F.Cu") (uuid "e")))+  (footprint "Capacitor_SMD:C_0402" (property "Reference" "C1") (property "Value" "100n")+    (pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu") (net "VCC")) (pad "2" smd rect (at 1 0) (size 1 1) (layers "F.Cu") (net "GND")))+  (footprint "Connector:J" (property "Reference" "J1") (property "Value" "HDR")+    (pad "1" thru_hole circle (at 0 0) (size 1 1) (drill 0.6) (layers "*.Cu") (net 7 "/USART1_TX"))))+'''++def main():+    p = '/tmp/alternate-part-test.kicad_pcb'; open(p, 'w').write(BOARD)+    mpn, ref, used, pads = A.board_pins(p, ref='U1')+    assert (mpn, ref, pads) == ('STM32F411RET6', 'U1', 5), (mpn, ref, pads)+    got = {u['pin']: u['net'] for u in used}+    # pin 1 shares VCC with C1; 42 shares /USART1_TX with J1; 43 is alone on its net; 44 is unconnected; 45 has no net+    assert got == {'1': 'VCC', '42': '/USART1_TX'}, got+    assert A.board_pins(p, mpn='stm32f411ret6')[1] == 'U1'+    # the answer always says its tier and how sure it is, and where every stock number came from (one real case per+    # tier, saved from the live route on 2026-10-30)+    F = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fixtures')+    load = lambda m: json.load(open(os.path.join(F, m + '.json')))+    t1 = A.render(load('STM32U5A5VJT6'))+    assert t1.splitlines()[0].endswith('tier 1, confidence high'), t1.splitlines()[0]+    assert 'stock: Mouser' in t1 and '(cached' in t1, 'tier 1: stock with its source'+    t2 = A.render(load('LM358DR'))+    assert 'tier 2, confidence medium-high' in t2.splitlines()[0] and 'snapshot 2026-04-01' in t2, 'tier 2: a JLCPCB number says it is a snapshot'+    t3 = A.render(load('2N7002'))+    assert 'tier 3' in t3.splitlines()[0] and 'warning:' in t3 and '(JSMSEMI)' in t3, 'tier 3: warnings and the maker shown'+    t4 = A.render(load('TPS54202DDCR'))+    assert 'Do the check yourself' in t4 and '1. List same-package candidates' in t4, 'no verified candidate: the method, step by step'+    print('ok: 6 tests')++if __name__ == '__main__':+    main()
uninstall.shadded+4
@@ -0,0 +1,4 @@+#!/bin/sh+set -eu+SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)+exec python3 "$SCRIPT_DIR/scripts/uninstall.py" "$@"