← Commit history

v0.1.26: vendor part number house rule as a check, schematic params

John Lauer ·1a2499e8cd ·1mo ago ·parent d56798c
8 files changed +127−12
CHANGELOG.md+7
@@ -1,5 +1,12 @@ # Changelog +## 0.1.26 — 2026-09-01+- **The Adom house rule is now a check.** `vendor_part_numbers` (warning) flags any purchased component missing Mouser / DigiKey / LCSC part numbers — Mouser named first because it is the preferred vendor, so if only one gets filled in, that is the one. Test points, fiducials and mounting holes are exempt: they are not purchased parts, and flagging them would train people to ignore the check.+- `missing_datasheets` (info) and `schematic_placeholder_values` (error) — the latter catches default Comments at the SOURCE, where the BOM is actually generated from, rather than only on placed parts.+- **The schematic read now carries component parameters** (`params: "1"`), using the same `MkSet(eParameter)` iterator the parameter WRITER has always used — so three checks arrived with no new Altium API surface. Capped at 40 parameters per component.+- The three schematic checks return nothing unless `includeSchematic: true`, since without a sheet read they have nothing to judge.+- 10 checks.+ ## 0.1.25 — 2026-09-01 - **`checks:[...]` now filters on the ID you actually see.** The selector was derived from the Python function name (`_chk_components_outside_board`) while findings report `components_outside_board_outline`, so filtering by the visible name silently ran ZERO checks and returned a clean review. Each check now declares its own ID, the reply lists `availableChecks`, and an unrecognised name comes back as `unknownChecks` instead of being ignored. - **New verb `altium_read_board`** — the raw read (components, nets, layers, copper, rules, occupancy, outline) in one round trip. It existed only in the Rust bridge, which meant nobody could see what a check saw; a review's silence had to be taken on trust. Now it can be inspected directly.
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.25",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.25/adom-bridge-altium-v0.1.25.zip",-  "sha256": "480d7fea5b48f0b5423096278512b2a4f597f04674806a095de31e6a464904b1",-  "size": 124534,+  "version": "0.1.26",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.26/adom-bridge-altium-v0.1.26.zip",+  "sha256": "4d39499ee8bbf1af001b29043920415852e95cc8d9820393595f49fed75558b7",+  "size": 125669,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.26.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.25",+  "version": "0.1.26",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.1.25",+  "version": "0.1.26",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.25+0.1.26
src/agent/AdomAgent.pas.tmpl+23−4
@@ -3327,15 +3327,18 @@ Var     PIt  : ISch_Iterator;     Comp : ISch_Component;     Pin  : ISch_Pin;-    Want, S, PS : String;-    N, Limit : Integer;-    WantPins : Boolean;+    Prm  : ISch_Parameter;+    QIt  : ISch_Iterator;+    Want, S, PS, MS : String;+    N, Limit, PN : Integer;+    WantPins, WantParams : Boolean; Begin     Doc := SchServer.GetCurrentSchDocument;     If Doc = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current schematic document (open a .SchDoc first)') + '}'; Exit; End;     Want := Param(Lines, 'designator');     Limit := StrToIntDef(Param(Lines, 'limit'), 500);     WantPins := Param(Lines, 'pins') <> '0';+    WantParams := Param(Lines, 'params') = '1';     S := ''; N := 0;     Iter := Doc.SchIterator_Create;     Iter.AddFilter_ObjectSet(MkSet(eSchComponent));@@ -3359,8 +3362,24 @@ Begin                 End;                 Comp.SchIterator_Destroy(PIt);             End;+            MS := '';+            If WantParams Then+            Begin+                PN := 0;+                QIt := Comp.SchIterator_Create;+                QIt.AddFilter_ObjectSet(MkSet(eParameter));+                Prm := QIt.FirstSchObject;+                While (Prm <> Nil) And (PN < 40) Do+                Begin+                    If MS <> '' Then MS := MS + ',';+                    MS := MS + '{' + JStr('name', Prm.Name) + ',' + JStr('value', Prm.Text) + '}';+                    PN := PN + 1;+                    Prm := QIt.NextSchObject;+                End;+                Comp.SchIterator_Destroy(QIt);+            End;             If S <> '' Then S := S + ',';-            S := S + '{' + JStr('designator', Comp.Designator.Text) + ',' ++            S := S + '{' + JRaw('params', '[' + MS + ']') + ',' + JStr('designator', Comp.Designator.Text) + ',' +                 JRaw('partId', IntToStr(Comp.CurrentPartID)) + ',' +                 JStr('libReference', Comp.LibReference) + ',' +                 JStr('comment', Comp.Comment.Text) + ',' +
src/server.py+90−1
@@ -1386,6 +1386,7 @@ def h_read_schematic(args):     if g:         return g     r = agent.call("read_schematic", {"sections": args.get("sections") or "components",+                                      "params": "1" if args.get("params", True) else "0",                                       "limit": args.get("limit", 500)},                    timeout=float(args.get("timeout", 90)))     data = r.get("data") or {}@@ -1630,6 +1631,89 @@ def _chk_components_near_board_edge(board, sheet, margin_mil=20):     return out  ++# Adom house rule (John, standing): Mouser is the PREFERRED vendor, and every component carries+# Mouser + DigiKey + LCSC part numbers. Encoded as a check so a board can be held to it, rather+# than the rule living only in people's heads.+VENDOR_PATTERNS = {+    "Mouser": ("mouser",),+    "DigiKey": ("digikey", "digi-key", "digi key"),+    "LCSC": ("lcsc", "jlcpcb"),+}+# Designator prefixes that legitimately carry no vendor part number.+NON_PURCHASED_PREFIXES = ("TP", "FID", "MH", "H", "LOGO", "NET", "PWR", "GND")+++def _params_of(comp):+    return {(p.get("name") or "").strip().lower(): (p.get("value") or "").strip()+            for p in (comp.get("params") or []) if p.get("name")}+++def _is_purchased(designator):+    d = (designator or "").upper()+    prefix = "".join(ch for ch in d if ch.isalpha())+    return prefix not in NON_PURCHASED_PREFIXES+++def _chk_vendor_part_numbers(board, sheet):+    """warning — a purchased component missing Mouser / DigiKey / LCSC part numbers.++    Mouser first in the message because it is the preferred vendor: if only one is going to be+    filled in, that is the one. Test points, fiducials and mounting holes are exempt — they are not+    purchased parts and flagging them would train people to ignore the check."""+    if not sheet:+        return []+    out = []+    for c in (sheet.get("components") or {}).get("components") or []:+        des = c.get("designator")+        if not _is_purchased(des):+            continue+        params = _params_of(c)+        blob = " ".join(f"{k} {v}" for k, v in params.items())+        missing = [vendor for vendor, keys in VENDOR_PATTERNS.items()+                   if not any(k in blob for k in keys)]+        if missing:+            out.append({"check": "vendor_part_numbers", "severity": "warning", "designator": des,+                        "missing": missing,+                        "message": f"{des} has no {' / '.join(missing)} part number "+                                   f"(house rule: Mouser preferred, all three required)."})+    return out+++def _chk_missing_datasheets(board, sheet):+    """info — a purchased component with no datasheet link."""+    if not sheet:+        return []+    out = []+    for c in (sheet.get("components") or {}).get("components") or []:+        des = c.get("designator")+        if not _is_purchased(des):+            continue+        params = _params_of(c)+        if not any(k in ("datasheet", "helpurl", "datasheet link", "datasheeturl") or "datasheet" in k+                   for k in params):+            out.append({"check": "missing_datasheets", "severity": "info", "designator": des,+                        "message": f"{des} has no Datasheet parameter."})+    return out+++def _chk_schematic_placeholders(board, sheet):+    """error — a schematic component still carrying default text in Comment.++    The board-side check only sees placed parts; this catches it at the source, where it is+    cheaper to fix and where the BOM is actually generated from."""+    if not sheet:+        return []+    out = []+    for c in (sheet.get("components") or {}).get("components") or []:+        com = (c.get("comment") or "").strip()+        if com.lower() in PLACEHOLDER_VALUES:+            out.append({"check": "schematic_placeholder_values", "severity": "error",+                        "designator": c.get("designator"),+                        "message": f"{c.get('designator')} on the sheet has placeholder Comment {com!r}."})+    return out++ # (id, fn). The ID is what appears in a finding's `check` field, so `checks:["..."]` filters on the # name the caller actually sees. Deriving it from the Python function name instead was a trap: the # function is `_chk_components_outside_board`, the finding says `components_outside_board_outline`,@@ -1642,6 +1726,10 @@ BOARD_CHECKS = [     ("duplicate_designators", _chk_duplicate_designators),     ("components_outside_board_outline", _chk_components_outside_board),     ("components_near_board_edge", _chk_components_near_board_edge),+    # schematic-side: need includeSchematic:true, and quietly return nothing without it+    ("vendor_part_numbers", _chk_vendor_part_numbers),+    ("missing_datasheets", _chk_missing_datasheets),+    ("schematic_placeholder_values", _chk_schematic_placeholders), ]  # Named so a clean result is not mistaken for a complete one.@@ -1674,7 +1762,8 @@ def h_review_board(args):         return r     sheet = None     if args.get("includeSchematic"):-        sr = agent.call("read_schematic", {"sections": "components"}, timeout=float(args.get("timeout", 90)))+        sr = agent.call("read_schematic", {"sections": "components", "params": "1"},+                        timeout=float(args.get("timeout", 90)))         sd = sr.get("data") or {}         sheet = sd if sd.get("ok") else None     wanted = args.get("checks")