app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
2.14.3: LED row was throwing on every refresh (shortState, busySince); build now gated on behaviour
8 files changed
+266−10
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.14.2"+version = "2.14.3" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time."
VERSION+1−1@@ -1 +1 @@-2.14.2+2.14.3
build.sh+17@@ -35,6 +35,12 @@ if [ "$V" != "$CARGO_V" ] || [ "$V" != "$PKG_V" ]; then fi echo "[build] version $V agrees across VERSION, Cargo.toml and package.json" +# A missing function is invisible to a byte-check. 2.14.0 shipped with api() deleted by an+# over-greedy regex: the markup and CSS were correct, every call site swallowed its own+# error, and the dashboard rendered an empty panel instead of failing. This gate fails the+# build on any identifier that is CALLED in manage.html and DEFINED nowhere.+python3 tools/js-call-check.py src/manage.html || exit 1+ echo "[build] test" cargo test --release --quiet @@ -57,6 +63,17 @@ HITS="$(grep -a -c BABYLON bin/adom-lbr || true)" [ "${HITS:-0}" -lt 10 ] || { echo "[build] FAIL the viewer is back INSIDE the binary (BABYLON hits=$HITS); it should ship in assets/" >&2; exit 1; } grep -q 'assets/\*\*' package.json || { echo "[build] FAIL package.json files[] does not ship assets/**" >&2; exit 1; } +# Behaviour, not bytes: run the REAL page against the REAL binary and assert what a person+# would look at (prompt rows, the two icon buttons, the LED row, the demo button), with the+# page's own swallowing catches made to confess. Byte-checks passed while 2.14.0 and 2.14.2+# shipped broken; this is the check that would have caught both.+if [ -d tools/node_modules ]; then+ NODE_PATH=tools/node_modules node tools/page-smoke.js || exit 1+else+ echo "[smoke] NOT RUN: tools/node_modules is missing, so the page's BEHAVIOUR is unverified." >&2+ echo " run: (cd tools && npm install)" >&2+fi+ printf '[build] OK %s binary %.2f MB + assets %.2f MB (was one %.2f MB binary)\n' "$V" \ "$(stat -c%s bin/adom-lbr | awk '{print $1/1048576}')" \ "$(du -sb assets | awk '{print $1/1048576}')" \
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.14.2",+ "version": "2.14.3", "type": "app", "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.", "dependencies": {},
src/manage.html+29−7@@ -60,7 +60,7 @@ #say .say-sub{margin:4px 0 14px;font:13.5px var(--brand-body);color:var(--muted);max-width:70ch} #say .say-sub b{color:var(--teal);font-weight:700} .say-close{position:absolute;top:12px;right:12px;width:28px;height:28px}- .say-cols{display:grid;grid-template-columns:minmax(180px,220px) 1fr;gap:24px;align-items:start}+ .say-cols{display:grid;grid-template-columns:clamp(180px,30%,220px) 1fr;gap:24px;align-items:start} .say-right{border-left:1px solid var(--line);padding-left:24px;min-width:0} #btnDemo{display:inline-flex;align-items:center;gap:8px;background:var(--teal);color:var(--on-accent);border:none;border-radius:8px;padding:10px 18px;font:600 14px var(--brand-heading);cursor:pointer;transition:filter .12s,transform .06s} #btnDemo:hover:not(:disabled){filter:brightness(1.08)}@@ -79,12 +79,18 @@ .actbtn:hover{color:var(--teal);border-color:var(--line);background:rgba(0,184,177,.08)} .actbtn:active{transform:translateY(1px)} .actbtn:focus-visible{outline:2px solid var(--teal);outline-offset:2px}- /* 620px, not 760px (adom-ui-design 1.25.0): John's dock pane measures 744px,- inside the 744-853 band, so a 760px breakpoint collapsed the two columns in- the very pane the panel was designed for. 620 clears the band and still- collapses on a phone. A breakpoint bug is invisible to any test matrix- whose widths straddle it, which is why 744 is now in mine. */- @media (max-width:620px){+ /* 700px, the fleet number (adom-ui-design 1.34.0). I shipped 620 earlier, derived+ against John's 744px rail-CLOSED pane; that was a unilateral divergence made from a+ stale reading of the skill. It also manufactured the worst layout in the matrix: his+ pane is 632px with the dock rail OPEN, and 620 kept TWO columns there, squeezing the+ phrase cell to 234px. At 700 that state collapses to one column and the phrase cell+ recovers to 478px, because a single column reclaims the left track AND the 24px gap.+ The narrowest phrase cell anywhere is just ABOVE the threshold (338px at a 702px+ viewport), so the truncation risk lives at the BOTTOM of the two-column band, not the+ top; John's resting 744px sits 44px into it at 367px. Widths 632 and 744 are both in+ my matrix now, with the rail state recorded, since a breakpoint bug is invisible to+ any matrix whose widths straddle it. */+ @media (max-width:700px){ .say-cols{grid-template-columns:1fr} .say-right{border-left:none;border-top:1px solid var(--line);padding-left:0;padding-top:16px} #btnDemo{width:100%;justify-content:center}@@ -758,6 +764,22 @@ function toast(msg,err,kind){ t.addEventListener('mouseenter',()=>clearTimeout(timer)); function fade(el){el.classList.add('out');setTimeout(()=>el.remove(),160);} }+// The LED's short label comes from the server (dock.rs led_short), which sends a `short`+// on every LED: "v2.14.3", "1525 parts", "not answering". This helper went missing in an+// edit and paintLeds called it anyway, so the whole LED row threw on every refresh. Found+// by tools/js-call-check.py, not by eye.+function shortState(l){return l.short||l.state||'';}+// How long an LED has been amber, so a warn that never clears can say "stuck?" instead of+// pretending to be a normal busy state. Declared here because it is state that must SURVIVE+// between refreshes; it was referenced but never declared, which threw inside paintLeds on+// every single status refresh and left the header showing one red "dashboard not answering"+// pill while the server was perfectly healthy.+let busySince={};+// The last /api/status payload, read by the settings dialog. It used to be an IMPLICIT+// global created by its own assignment, which works only while that assignment runs first:+// when the status call threw, the settings dialog hit a ReferenceError instead of simply+// finding nothing. Declared, so it is null rather than absent.+let lastStatus=null; function paintLeds(st){ const row=document.getElementById('ledrow');row.innerHTML=''; (st.leds||[]).forEach(l=>{
tools/js-call-check.pyadded+122@@ -0,0 +1,122 @@+#!/usr/bin/env python3+"""Catch the 2.14.0 bug class: a function that is CALLED but no longer DEFINED.++An over-greedy regex during the toast rewrite deleted the api() helper. Every byte-check+passed, because the markup and CSS were genuinely correct, and every call site swallowed+its own error, so the page rendered an empty panel instead of failing. Only a read-only+eval inside John's live webview found it. This script finds it at build time instead.++It is deliberately conservative: it only reports a name that is called like a function+and is defined NOWHERE in the page and is not a known global. False negatives are fine;+a false positive that blocks a build is not.+"""+import re, sys, pathlib++src = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "src/manage.html").read_text()+scripts = re.findall(r"<script\b[^>]*>(.*?)</script>", src, re.S)+if not scripts:+ print("[jscheck] FAIL no <script> blocks found"); sys.exit(1)+js = "\n".join(scripts)++# Strip comments and strings with a single left-to-right scanner. Per-quote regex+# stripping is WRONG: an apostrophe inside a double-quoted string ("don't") makes the+# single-quote pass eat everything up to the next apostrophe, deleting real definitions+# and reporting them as missing. That false-positive storm is exactly how this script+# first failed, so the scanner walks the text once and honours whichever quote opened.+def strip_js(t):+ out = []; i = 0; n = len(t)+ while i < n:+ c = t[i]+ if c == "/" and i + 1 < n and t[i+1] == "/":+ while i < n and t[i] != "\n": i += 1+ elif c == "/" and i + 1 < n and t[i+1] == "*":+ i += 2+ while i + 1 < n and not (t[i] == "*" and t[i+1] == "/"): i += 1+ i += 2+ elif c in "'\"`":+ q = c; i += 1+ while i < n:+ if t[i] == "\\": i += 2; continue+ if t[i] == q: i += 1; break+ # a ${ } inside a template literal holds real code: keep it+ if q == "`" and t[i] == "$" and i + 1 < n and t[i+1] == "{":+ depth = 1; i += 2; start = i+ while i < n and depth:+ if t[i] == "{": depth += 1+ elif t[i] == "}": depth -= 1+ i += 1+ out.append(strip_js(t[start:i-1]))+ continue+ i += 1+ out.append('""')+ else:+ out.append(c); i += 1+ return "".join(out)++stripped = strip_js(js)++defined = set()+defined |= set(re.findall(r"function\s+([A-Za-z_$][\w$]*)", stripped))+defined |= set(re.findall(r"(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:function|\()", stripped))+# a declaration list declares EVERY name in it: `let sx,sy,ox,oy,drag=false` declares five,+# and matching only the first is how this script once reported four real declarations as+# implicit globals+for m in re.finditer(r"\b(?:const|let|var)\s+", stripped):+ i = m.end(); depth = 0; buf = []+ while i < len(stripped):+ c = stripped[i]+ if c in "([{": depth += 1+ elif c in ")]}":+ if depth == 0: break+ depth -= 1+ elif c == ";" and depth == 0: break+ elif c == "\n" and depth == 0 and not buf[-1:] == [","]: pass+ buf.append(c); i += 1+ for part in re.split(r",(?![^([{]*[)\]}])", "".join(buf)):+ name = re.match(r"\s*([A-Za-z_$][\w$]*)", part.split("=")[0])+ if name: defined.add(name.group(1))+ # destructuring: `const {a,b} = x` / `const [a,b] = x`+ defined |= set(re.findall(r"[A-Za-z_$][\w$]*", part.split("=")[0])) if part.strip()[:1] in "{[" else set()+defined |= set(re.findall(r"([A-Za-z_$][\w$]*)\s*:\s*(?:async\s*)?function", stripped))+defined |= set(re.findall(r"function\s*\(([^)]*)\)", stripped) and [])+for params in re.findall(r"function[^(]*\(([^)]*)\)", stripped):+ defined |= {p.strip().split("=")[0].strip() for p in params.split(",") if p.strip()}+for params in re.findall(r"\(([^()]*)\)\s*=>", stripped):+ defined |= {p.strip().split("=")[0].strip() for p in params.split(",") if p.strip()}+defined |= set(re.findall(r"([A-Za-z_$][\w$]*)\s*=>", stripped))+defined |= set(re.findall(r"catch\s*\(\s*([A-Za-z_$][\w$]*)", stripped))+# object-literal method shorthand -- `log(line){...}` inside an object is a DEFINITION,+# and it reads exactly like a call site if you only pattern-match on NAME(+defined |= set(re.findall(r"[{,]\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{", stripped))++GLOBALS = set("""+window document console fetch setTimeout clearTimeout setInterval clearInterval+JSON Math Date Object Array String Number Boolean Promise Error Map Set RegExp+parseInt parseFloat isNaN encodeURIComponent decodeURIComponent alert confirm prompt+AbortController FormData URL URLSearchParams Image Audio Blob File FileReader+requestAnimationFrame cancelAnimationFrame getComputedStyle matchMedia structuredClone+addEventListener removeEventListener dispatchEvent+localStorage sessionStorage navigator location history CustomEvent Event MutationObserver+IntersectionObserver ResizeObserver TextEncoder TextDecoder btoa atob queueMicrotask+Intl Symbol Proxy Reflect BigInt WeakMap WeakSet ArrayBuffer Uint8Array+if for while switch catch return typeof function do else new delete void await yield+async of in instanceof throw case try finally+""".split())++called = set(re.findall(r"(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(", stripped))+missing = sorted(n for n in called if n not in defined and n not in GLOBALS)++# Second class: an IMPLICIT GLOBAL, a bare `name = value` with no declaration anywhere.+# It works only while its assignment happens to run before every read, so it fails exactly+# when something upstream has already gone wrong, turning one fault into two.+assigns = set(re.findall(r"(?:^|[;{}\n])\s*([A-Za-z_$][\w$]*)\s*=(?!=)", stripped))+implicit = sorted(n for n in assigns if n not in defined and n not in GLOBALS)+if implicit:+ print("[jscheck] FAIL assigned but never declared (implicit global): " + ", ".join(implicit))+ sys.exit(1)++if missing:+ print("[jscheck] FAIL called but never defined: " + ", ".join(missing))+ print("[jscheck] this is the 2.14.0 bug class (a helper deleted by an edit).")+ sys.exit(1)+print(f"[jscheck] OK {len(called)} distinct call targets, all defined")
tools/package.jsonadded+6@@ -0,0 +1,6 @@+{+ "name": "adom-lbr-tools",+ "private": true,+ "description": "Build-time checks for adom-lbr. jsdom runs the real dashboard page so a missing function fails the build instead of shipping as an empty panel.",+ "devDependencies": { "jsdom": "^22.1.0" }+}
tools/page-smoke.jsadded+89@@ -0,0 +1,89 @@+#!/usr/bin/env node+/* Behavioural smoke test for the dashboard page.+ *+ * WHY THIS EXISTS. 2.14.0 shipped with api() deleted by an over-greedy regex and 2.14.2+ * shipped with shortState() and busySince missing. All three were invisible to 22 byte+ * checks, because the markup and the CSS were genuinely correct. They were invisible to+ * review. And they were invisible to the user too, because every call site catches its own+ * error: the page answered with an empty prompts panel and a single red "dashboard not+ * answering" pill while the server was serving 200s in four milliseconds.+ *+ * A structural audit cannot see a missing function. This runs the REAL page against the+ * REAL server and asserts what a person would look at.+ *+ * Requires jsdom (npm i jsdom@22 inside tools/). No layout engine, so this proves+ * BEHAVIOUR, not pixels; widths still have to be measured in a real webview.+ */+const { JSDOM, VirtualConsole } = require('jsdom');+const { spawn, execSync } = require('child_process');++const PORT = process.env.SMOKE_PORT || '8971';+const URL = `http://127.0.0.1:${PORT}/`;+const BIN = process.env.SMOKE_BIN || './target/release/adom-lbr';++const EXPECT = {+ sayRows: n => n >= 3 && n <= 6, // newbie-panel: 3 to 6 rows, never zero+ copyButtons: n => n >= 6, // two icon buttons per row+ ledPills: n => n >= 5, // Binary, Wiki, Identity, Library, 3D viewer, Dock+ demoButton: v => v === true,+};++(async () => {+ const srv = spawn(BIN, ['dashboard', 'serve', '--port', String(PORT), '--foreground'],+ { stdio: 'ignore', detached: true });+ const done = code => { try { process.kill(-srv.pid); } catch (e) {} process.exit(code); };++ for (let i = 0; i < 40; i++) {+ try { await fetch(URL); break; } catch (e) { await new Promise(r => setTimeout(r, 250)); }+ }++ const errs = [];+ const vc = new VirtualConsole();+ vc.on('jsdomError', e => errs.push(String(e.message || e)));++ let html = await (await fetch(URL)).text();+ // Make the page's own swallowing catches confess, in this COPY only. A caught error is+ // still a defect; the user just never gets told about it.+ html = html.replace(/\}catch\((\w+)\)\{/g, '}catch($1){ if(typeof console!=="undefined")console.error("SWALLOWED >>> "+($1&&$1.stack?$1.stack:$1));');+ vc.on('error', (...a) => { const s = a.join(' '); if (/SWALLOWED/.test(s)) errs.push(s.split('\n')[0]); });++ const dom = new JSDOM(html, {+ url: URL, runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc,+ beforeParse(w) {+ w.fetch = (p, o) => fetch(new w.URL(p, URL).href, o);+ w.scrollTo = () => {};+ w.matchMedia = w.matchMedia || (q => ({ matches: false, media: q, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){} }));+ }+ });+ const d = dom.window.document;+ await new Promise(r => setTimeout(r, 4000));++ const got = {+ sayRows: d.querySelectorAll('.say-row').length,+ copyButtons: d.querySelectorAll('.actbtn').length,+ ledPills: d.querySelectorAll('.led-pill').length,+ demoButton: !!d.getElementById('btnDemo'),+ };+ const ledText = [...d.querySelectorAll('.led-pill')].map(p => p.textContent.replace(/\s+/g, ' ').trim());++ let bad = 0;+ for (const [k, test] of Object.entries(EXPECT)) {+ const ok = test(got[k]);+ if (!ok) bad++;+ console.log(`[smoke] ${ok ? 'ok ' : 'FAIL'} ${k} = ${got[k]}`);+ }+ // The specific shape of the 2.14.2 bug: the LED row falls back to one red pill.+ if (ledText.length === 1 && /not answering/.test(ledText[0])) {+ console.log('[smoke] FAIL LED row is the error fallback: ' + ledText[0]); bad++;+ }+ const real = errs.filter(e => /ReferenceError|TypeError|SyntaxError/.test(e));+ if (real.length) {+ bad++;+ console.log('[smoke] FAIL javascript errors on load (including ones the page swallows):');+ [...new Set(real)].slice(0, 6).forEach(e => console.log(' ' + e.slice(0, 160)));+ }+ console.log('[smoke] leds: ' + (ledText.join(' | ') || '(none)'));+ console.log(bad ? `[smoke] FAILED ${bad} check(s)` : '[smoke] OK page renders and answers');+ dom.window.close();+ done(bad ? 1 : 0);+})().catch(e => { console.log('[smoke] FAIL ' + e.message); process.exit(1); });