← Commit history
CHANGELOG.md+9
@@ -1,5 +1,14 @@ # Changelog +## 0.1.28 — 2026-09-01+- **Four design-rule hygiene checks**, all from the rules the board read already carries:+  - `disabled_design_rules` — a rule that exists but is switched OFF. Worse than a missing rule: the board LOOKS ruled, DRC passes, and the constraint was never applied. Raised to error for Clearance / Width / HoleSize / ShortCircuit.+  - `missing_critical_rules` (error) — no ENABLED rule of a kind a fabricator relies on.+  - `duplicate_rule_priority` (info) — two enabled rules of one kind sharing a priority. Altium resolves the tie internally, so which one wins is not readable from the board; usually a copy-paste never re-prioritised.+  - `components_without_footprint` (warning) — a placed part with an empty footprint pattern name, so it cannot be matched back to a library or checked for updates.+- Rule kinds are named from the ordinal table read off a live default rule set; an unknown ordinal renders as `kind <n>` rather than being invented.+- 14 checks.+ ## 0.1.27 — 2026-09-01 - **`review_board` switches documents and returns PARTIAL results.** Altium holds one current document, so a board read and a sheet read cannot both be "now" — with a schematic focused, the previous version failed the whole review because `read_board` had nothing current. It now takes optional `board` and `sheet` paths and activates each in turn (like `cross_check`), and when one document cannot be read it still runs the checks that CAN run. - **`skipped` names every check that did not run and why** — "no board read", "no schematic read — pass includeSchematic:true". A review that silently drops half its checks reads exactly like a clean board, which is the failure mode worth engineering against.
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.27",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.27/adom-bridge-altium-v0.1.27.zip",-  "sha256": "14e69c2e7d8543c9ae4a87ffc415d422ed124e4f32c8457cc558ece41ae89e93",-  "size": 126211,+  "version": "0.1.28",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.28/adom-bridge-altium-v0.1.28.zip",+  "sha256": "8cf6cda116e08da60816540c274409aeb752f5125b9135d7ff4bbfb21db09051",+  "size": 127694,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.28.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.27",+  "version": "0.1.28",   "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.27",+  "version": "0.1.28",   "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.27+0.1.28
src/server.py+96−1
@@ -1714,6 +1714,96 @@ def _chk_schematic_placeholders(board, sheet):     return out  ++# Altium's TRuleKind ordinals, read off a live AD26 default rule set (each default rule is named+# after its own kind, which makes the mapping observable rather than guessed). Unknown ordinals are+# rendered as "kind <n>" — never invented.+RULE_KIND_NAMES = {+    0: "Clearance", 2: "Width", 6: "PlaneConnect", 7: "RoutingTopology", 8: "RoutingPriority",+    9: "RoutingLayers", 10: "RoutingCorners", 11: "RoutingVias", 12: "PlaneClearance",+    13: "SolderMaskExpansion", 14: "PasteMaskExpansion", 15: "ShortCircuit", 16: "UnRoutedNet",+    20: "PolygonConnect", 24: "ComponentClearance", 42: "HoleSize", 43: "FabricationTestpoint",+    44: "FabricationTestPointUsage", 48: "LayerPairs", 49: "Fanout", 50: "Height",+    51: "DiffPairsRouting", 52: "HoleToHoleClearance", 53: "MinimumSolderMaskSliver",+    54: "SilkToSolderMaskClearance", 55: "SilkToSilkClearance", 56: "NetAntennae",+    57: "AssemblyTestpoint", 58: "AssemblyTestPointUsage", 59: "SilkToBoardRegionClearance",+    62: "UnpouredPolygon",+}+# Rules whose absence a fabricator will notice before you do.+CRITICAL_RULE_KINDS = {0: "Clearance", 2: "Width", 42: "HoleSize", 15: "ShortCircuit"}+++def _rule_kind_name(k):+    return RULE_KIND_NAMES.get(k, f"kind {k}")+++def _chk_disabled_design_rules(board, sheet):+    """warning — a design rule that exists but is switched OFF.++    A disabled Clearance or ShortCircuit rule is worse than a missing one: the board LOOKS ruled,+    DRC passes, and the constraint was never applied."""+    out = []+    for r in (board.get("rules") or {}).get("rules") or []:+        if r.get("enabled") is False:+            kind = _rule_kind_name(r.get("kind"))+            sev = "error" if r.get("kind") in CRITICAL_RULE_KINDS else "warning"+            out.append({"check": "disabled_design_rules", "severity": sev, "rule": r.get("name"),+                        "message": f"Design rule {r.get('name')!r} ({kind}) is DISABLED — DRC will pass "+                                   f"without ever applying it."})+    return out+++def _chk_missing_critical_rules(board, sheet):+    """error — no ENABLED rule of a kind a fabricator relies on."""+    rules = (board.get("rules") or {}).get("rules") or []+    if not rules:+        return []  # no rule data at all is not the same as no rules+    have = {r.get("kind") for r in rules if r.get("enabled") is not False}+    out = []+    for kind, name in sorted(CRITICAL_RULE_KINDS.items()):+        if kind not in have:+            out.append({"check": "missing_critical_rules", "severity": "error", "rule": name,+                        "message": f"No enabled {name} rule on this board."})+    return out+++def _chk_duplicate_rule_priority(board, sheet):+    """info — two enabled rules of the same kind sharing a priority.++    Altium resolves ties by internal order, so which one wins is not something you can read off the+    board. Usually a copy-paste that was never re-prioritised."""+    seen = {}+    for r in (board.get("rules") or {}).get("rules") or []:+        if r.get("enabled") is False:+            continue+        key = (r.get("kind"), r.get("priority"))+        seen.setdefault(key, []).append(r.get("name"))+    out = []+    for (kind, prio), names in sorted(seen.items(), key=lambda kv: (str(kv[0][0]), str(kv[0][1]))):+        if len(names) > 1:+            out.append({"check": "duplicate_rule_priority", "severity": "info",+                        "rule": ", ".join(sorted(names)),+                        "message": f"{len(names)} enabled {_rule_kind_name(kind)} rules share priority "+                                   f"{prio} ({', '.join(sorted(names))}) — which one wins is not readable "+                                   f"from the board."})+    return out+++def _chk_components_without_footprint(board, sheet):+    """warning — a placed component whose footprint PATTERN is empty.++    A placed part always has geometry, but an empty pattern name means the footprint has no identity:+    it cannot be matched back to a library, compared across a project, or checked for updates."""+    out = []+    for c in (board.get("components") or {}).get("components") or []:+        if not (c.get("pattern") or "").strip():+            out.append({"check": "components_without_footprint", "severity": "warning",+                        "designator": c.get("designator"),+                        "message": f"{c.get('designator')} has no footprint pattern name — it cannot be "+                                   f"matched back to a library."})+    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`,@@ -1730,6 +1820,11 @@ BOARD_CHECKS = [     ("vendor_part_numbers", _chk_vendor_part_numbers),     ("missing_datasheets", _chk_missing_datasheets),     ("schematic_placeholder_values", _chk_schematic_placeholders),+    # design-rule hygiene, from the rules the board read already carries+    ("disabled_design_rules", _chk_disabled_design_rules),+    ("missing_critical_rules", _chk_missing_critical_rules),+    ("duplicate_rule_priority", _chk_duplicate_rule_priority),+    ("components_without_footprint", _chk_components_without_footprint), ]  # Named so a clean result is not mistaken for a complete one.@@ -1774,7 +1869,7 @@ def h_review_board(args):         e = _open(args["board"])         if e:             return {"success": False, "stage": "board", **e}-    r = agent.call("read_board", {"sections": "components,nets,layers,copper,occupancy,outline",+    r = agent.call("read_board", {"sections": "components,nets,layers,copper,rules,occupancy,outline",                                   "limit": args.get("limit", 2000)}, timeout=timeout)     bd = r.get("data") or {}     if bd.get("ok"):