← Commit history
package.json+1−1
@@ -2,7 +2,7 @@   "name": "alternate-part",   "slug": "alternate-part",   "title": "Alternate Part",-  "version": "0.2.1",+  "version": "0.2.2",   "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.",
page.json+1−1
@@ -2,7 +2,7 @@   "name": "alternate-part",   "slug": "alternate-part",   "title": "Alternate Part",-  "version": "0.2.1",+  "version": "0.2.2",   "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.",
skills/alternate-part/SKILL.md+6−2
@@ -47,8 +47,12 @@ Tier 2 covers every part with an Adom page (`adom_pages.indexed` in the response      `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;+     older one (the refresh failed: say how old); `snapshot` is a JLCPCB catalog row, and its age is the row's own+     listing date (`fetched_at`), which can be years older than the catalog copy. The script prints `IN STOCK` only+     on a live answer and `stale stock` otherwise, and a NOTE when no live answer came back at all (the distributor+     services were down). Rank by live stock; never present a cached or catalog number as "in stock now" without+     its age, and tell the user to confirm on the distributor before ordering. A distributor missing from the line+     gave no number: that is unknown, not zero;    - `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
skills/alternate-part/scripts/alternates.py+11−3
@@ -95,7 +95,8 @@ def _age(ts):     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'+    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' if h < 24 * 365 \+        else f'{h / 24 / 365:.1f} years old'   def stock_line(st):@@ -105,7 +106,10 @@ def stock_line(st):         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'))}"+        # a catalog row carries its own listing date (as_of / fetched_at); the snapshot date only says when the catalog+        # was copied, so a 2024 listing in an April 2026 snapshot reads as 2024 (fresh-thread test, 2026-10-30)+        when = f"catalog, {_age(v.get('fetched_at') or v.get('as_of')) or 'date unknown'}" 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' @@ -122,12 +126,16 @@ def render(d):         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 and not any(any((v or {}).get('source') == 'live' for v in (c.get('stock') or {}).values()) for c in cands[:10]):+        lines.append('  NOTE: no live stock came back (the distributor services did not answer); every number below is an '+                     'older answer or the JLCPCB catalog, with its age. Confirm on the distributor before ordering.')     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      ')+        # 'in stock' only on a live answer; an older or catalog number says so, it is not a promise (2026-10-30)+        flag = 'IN STOCK   ' if live else ('stale 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', '')
skills/alternate-part/tests/run.py+5−2
@@ -34,12 +34,15 @@ def main():     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'+    assert 'tier 2, confidence medium-high' in t2.splitlines()[0] and '(catalog, ' in t2 and 'snapshot 2026-04-01' not in t2, \+        'tier 2: a JLCPCB number gives its own listing date, not the snapshot date'+    assert 'NOTE: no live stock came back' in t2 and 'stale stock' in t2 and 'IN STOCK' not in t2, \+        'no live answer: said up front, and nothing claims IN STOCK on old numbers'     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')+    print('ok: 8 tests')  if __name__ == '__main__':     main()