app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
John Lauer
Publish 0.2.48
7fce739
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
"""`sourcing check` through the CLI: any distributor passes on any profile as consigned parts, with one
warning and a suggestion per row. The lookup order per distinct MPN (issue #111): the wiki component
page first, then the pnp-inventory vendor stock cache, and a live adom-parts-search only for what those
did not answer. A fake adom-wiki (two component pages) and a fake adom-parts-search on PATH return
fixed JSON and log each call, so the test never touches the wiki or spends the shared vendor quota. It
proves: a page with the number and a dated stock count answers with no live search; a page without the
preferred source's number falls back to the live search; a number with no stock is "stock not
recorded"; both caches in the run folder (a rerun reads and searches nothing); AIFLOW_NO_PART_SEARCH=1
(no live search, pages still read) and AIFLOW_NO_PART_LOOKUP=1 (nothing). pnp-inventory is not on PATH,
so that step is skipped quietly. No CAD, desktop or dashboard."""
import json, os, pathlib, subprocess, sys, tempfile, datetime
binary = str(pathlib.Path(sys.argv[1]).resolve())
SEARCH = r'''#!/usr/bin/env python3
import json, os, sys
open(os.environ["FAKE_LOG"], "a").write(" ".join(sys.argv[1:]) + "\n")
mpn = sys.argv[2]
mouser = {"TPS54202DDCR": [{"mpn": "TPS54202DDCR", "vendor_pn": "595-TPS54202DDCR", "stock": 1200}],
"AP2112K-3.3TRG1": [{"mpn": "AP2112K-3.3TRG1", "vendor_pn": "621-AP2112K-3.3TRG1", "stock": 5000}]}.get(mpn, [])
jlc = {"TPS54202DDCR": [{"mpn": "TPS54202DDCR", "vendor_pn": "C191884", "stock": 299988, "flags": ["extended"]}]}.get(mpn, [])
print(json.dumps({"query": mpn, "mouser": {"ok": True, "hits": mouser}, "digikey": {"ok": True, "hits": []}, "jlcpcb": {"ok": True, "hits": jlc}}))
'''
# page.json the way real component pages store distributor numbers: a string, {part, url, stock, checked}, or false
WIKI = r'''#!/usr/bin/env python3
import json, os, sys
open(os.environ["FAKE_WIKI_LOG"], "a").write(" ".join(sys.argv[1:]) + "\n")
pages = {
"adom/tps54202ddcr": {"type": "component", "component": {"mpn": "TPS54202DDCR", "distributor_links": {
"mouser": {"part": "595-TPS54202DDCR", "url": "https://www.mouser.com/x", "stock": 160, "checked": "2026-10-20"},
"digikey": "296-TPS54202DDCRTR-ND", "lcsc": False}}},
"adom/ap2112k-3-3trg1": {"type": "component", "component": {"mpn": "AP2112K-3.3TRG1", "distributor_links": {
"mouser": "621-AP2112K-3.3TRG1",
"lcsc": {"part": "C51118", "url": "https://www.lcsc.com/x", "stock": 12535, "checked": "2026-10-19", "tier": "basic"}}}},
}
if sys.argv[1:3] == ["repo", "show"] and sys.argv[4:] == ["page.json"] and sys.argv[3] in pages:
print(json.dumps(pages[sys.argv[3]])); sys.exit(0)
print(json.dumps({"http_status": 404, "response": {"error": "Page not found", "code": "PAGE_NOT_FOUND"}}))
print("error [PAGE_NOT_FOUND]: no such page", file=sys.stderr); sys.exit(1)
'''
today = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d')
with tempfile.TemporaryDirectory(prefix='aiflow-sourcing-') as td:
p = pathlib.Path(td)
fakebin = p / 'bin'; fakebin.mkdir()
for name, body in (('adom-parts-search', SEARCH), ('adom-wiki', WIKI)):
f = fakebin / name; f.write_text(body); f.chmod(0o755)
log, wlog = p / 'searches.log', p / 'wiki.log'
# PATH: the fakes, then only what python3 needs (no real adom-wiki or pnp-inventory)
py = pathlib.Path(sys.executable).parent
env = dict(os.environ, AIFLOW_DASHBOARD='off', PATH=f"{fakebin}{os.pathsep}{py}{os.pathsep}/usr/bin{os.pathsep}/bin", FAKE_LOG=str(log), FAKE_WIKI_LOG=str(wlog))
env.pop('AIFLOW_NO_PART_SEARCH', None); env.pop('AIFLOW_NO_PART_LOOKUP', None)
def call(run, *args, extra=None):
return subprocess.run([binary, '--run', str(run), '--ai-thread', 'sourcing-test', *args], env=dict(env, **(extra or {})), capture_output=True, text=True, timeout=120)
def outcome(run): return json.loads((run / 'run.json').read_text())['outcomes']['sourcing']
def searches(): return log.read_text().splitlines() if log.exists() else []
def reads(): return wlog.read_text().splitlines() if wlog.exists() else []
bom = p / 'bom.csv'
bom.write_text('ref,value,mpn,manufacturer,source,vendor_pn,package,stock_checked_2026-10-27\n'
'U1,buck,TPS54202DDCR,TI,DigiKey,296-TPS54202DDCRTR-ND,SOT-23-6,900\n'
'U2,buck,TPS54202DDCR,TI,DigiKey,296-TPS54202DDCRTR-ND,SOT-23-6,900\n'
'U3,op amp,ABC123,Acme,Arrow,ABC123,SOIC-8,800\n'
'U4,LDO,AP2112K-3.3TRG1,Diodes,DigiKey,AP2112K-3.3TRG1DITR-ND,SOT-23-5,700\n'
'R1,10k,CR0402-FX-1002GLF,Bourns,Adom stock,,0402,5000\n')
run = p / 'run'
r = call(run, 'intake', '--prompt-time', '2026-10-27T00:00:00Z', '--engine', 'test'); assert r.returncode == 0, r.stdout + r.stderr
# AIFLOW_NO_PART_LOOKUP=1: no page read, no search, one note, still a pass
r = call(run, 'sourcing', 'check', '--bom', str(bom), '--qty', '1', extra={'AIFLOW_NO_PART_LOOKUP': '1'})
assert r.returncode == 0, r.stdout + r.stderr
assert 'part suggestions skipped (AIFLOW_NO_PART_LOOKUP=1)' in r.stdout and not searches() and not reads(), r.stdout
o = outcome(run); assert o['consignedRows']['count'] == 4 and o['suggestions'] == [], o
# AIFLOW_NO_PART_SEARCH=1: the wiki pages still answer, no live search
r = call(run, 'sourcing', 'check', '--bom', str(bom), '--qty', '1', extra={'AIFLOW_NO_PART_SEARCH': '1'})
assert r.returncode == 0, r.stdout + r.stderr
assert not searches(), searches()
# one read per distinct MPN: AP2112K-3.3TRG1 tries ap2112k-3.3trg1 first, then the wiki slug form
assert reads() == ['repo show adom/tps54202ddcr page.json', 'repo show adom/abc123 page.json',
'repo show adom/ap2112k-3.3trg1 page.json', 'repo show adom/ap2112k-3-3trg1 page.json'], reads()
assert 'suggestion: U1 (TPS54202DDCR, DigiKey): Mouser has it, 595-TPS54202DDCR, 160 in stock (wiki page adom/tps54202ddcr, checked 2026-10-20); sourcing it there saves a consignment shipment' in r.stdout, r.stdout
assert 'suggestion: U2 (TPS54202DDCR, DigiKey): Mouser has it, 595-TPS54202DDCR, 160 in stock (wiki page adom/tps54202ddcr' in r.stdout, r.stdout
assert 'suggestion: U4 (AP2112K-3.3TRG1, DigiKey): Mouser has it, 621-AP2112K-3.3TRG1 (wiki page adom/ap2112k-3-3trg1), stock not recorded: `adom-parts-search show "AP2112K-3.3TRG1"` checks it live; sourcing it there saves a consignment shipment' in r.stdout, r.stdout
assert 'live part searches skipped (AIFLOW_NO_PART_SEARCH=1) for 2 MPN(s) the wiki pages and the pnp-inventory cache did not fully answer: ABC123, AP2112K-3.3TRG1' in r.stdout, r.stdout
assert 'suggestion: U3' not in r.stdout, r.stdout # nothing known, nothing claimed
o = outcome(run)
assert o['suggestionLookups']['wikiReads'] == 3 and o['suggestionLookups']['wikiHits'] == 2 and o['suggestionLookups']['liveSearches'] == 0, o['suggestionLookups']
assert (run / 'sourcing-lookup-cache.json').is_file()
# fab with live search: the page answers TPS54202DDCR, so only the rest is searched
r = call(run, 'sourcing', 'check', '--bom', str(bom), '--qty', '1')
assert r.returncode == 0, r.stdout + r.stderr
assert len(reads()) == 4, reads() # the page answers came from the run's cache
assert sum('warning:' in l and 'consigned parts' in l for l in r.stdout.splitlines()) == 1, r.stdout
assert searches() == ['search ABC123', 'search AP2112K-3.3TRG1'], searches()
assert 'suggestion: U1 (TPS54202DDCR, DigiKey): Mouser has it, 595-TPS54202DDCR, 160 in stock (wiki page adom/tps54202ddcr, checked 2026-10-20)' in r.stdout, r.stdout
assert f'suggestion: U4 (AP2112K-3.3TRG1, DigiKey): Mouser has it, 621-AP2112K-3.3TRG1, 5000 in stock (live search, checked {today}); sourcing it there saves a consignment shipment' in r.stdout, r.stdout
assert 'suggestion: U3 (ABC123, Arrow): no exact match at Mouser (live search); `adom-parts-search show "op amp SOIC-8"` to look for an equivalent, or send this part as consigned' in r.stdout, r.stdout
lines = r.stdout.splitlines()
w = next(i for i, l in enumerate(lines) if 'warning:' in l and 'consigned parts' in l)
assert 'suggestion:' in lines[w + 1], lines[w:w + 3]
o = outcome(run)
assert o['pass'] and o['consignedRows']['count'] == 4 and len(o['suggestions']) == 4, o
assert o['suggestionLookups']['wikiFromCache'] == 3 and o['suggestionLookups']['liveSearches'] == 2, o['suggestionLookups']
assert {s['refs']: s['from'] for s in o['suggestions']} == {'U1': 'wiki', 'U2': 'wiki', 'U3': 'search', 'U4': 'search'}, o['suggestions']
assert (run / 'sourcing-search-cache.json').is_file()
# a rerun reads both caches: no page read, no search
r = call(run, 'sourcing', 'check', '--bom', str(bom), '--qty', '1')
assert r.returncode == 0 and len(searches()) == 2 and len(reads()) == 4, (searches(), reads())
# jlcpcb: the TPS54202DDCR page has no LCSC number (false), so it falls back to the live search;
# the AP2112K page has its LCSC number and dated stock, so no search for it
r = call(run, 'sourcing', 'check', '--bom', str(bom), '--qty', '1', '--profile', 'jlcpcb')
assert r.returncode == 0, r.stdout + r.stderr
assert searches()[2:] == ['search TPS54202DDCR', 'search CR0402-FX-1002GLF'], searches()
assert reads()[4:] == ['repo show adom/cr0402-fx-1002glf page.json'], reads()
assert 'are not JLCPCB/LCSC parts' in r.stdout, r.stdout
assert f'U1 (TPS54202DDCR, DigiKey): LCSC has it, C191884 (extended), 299988 in stock (live search, checked {today}); add it to the lcsc column to save a consignment shipment' in r.stdout, r.stdout
assert 'U4 (AP2112K-3.3TRG1, DigiKey): LCSC has it, C51118 (basic), 12535 in stock (wiki page adom/ap2112k-3-3trg1, checked 2026-10-19); add it to the lcsc column to save a consignment shipment' in r.stdout, r.stdout
assert outcome(run)['consignedRows']['count'] == 5 # the Adom stock row is consigned on jlcpcb
# a real problem still fails: no MPN
bad = p / 'bad.csv'; bad.write_text('ref,mpn,source,stock_checked_2026-10-27\nU1,,DigiKey,10\n')
r = call(run, 'sourcing', 'check', '--bom', str(bad), '--qty', '1')
assert r.returncode == 0 and 'FINDINGS' in r.stdout and 'U1: no MPN' in r.stdout, r.stdout
print('sourcing-suggestions: ok')