← Commit history
CHANGELOG.md+8
@@ -1,5 +1,13 @@ # Changelog +## 0.1.24 — 2026-09-01+- **`read_board` gains an `outline` section** — the board extent, read through the SAME guarded pattern the library reader already uses, because a library's internal board has a nil `BoardOutline` and touching `BoundingRectangle` on it halts the engine (measured 2026-08-30). It reports which source it used: `boardOutline` (the real outline's bounding box) or `boardBounds` (everything on the board, including strays outside it).+- **Two review checks unlocked by it**, both named for what they actually measure:+  - `components_outside_board_outline` (error) — a part outside the board's bounding box.+  - `components_near_board_edge` (warning) — a component ORIGIN within a margin of the edge (default 20 mil). Not called `pads_near_board_edge`: per-pad geometry is not in the board read, and the catalog's name would claim a precision this does not have.+- **Both go silent when the extent came from `boardBounds`.** That fallback includes stray primitives outside the real outline, so using it to declare a part "off-board" would produce confident false errors. A weaker source yields no finding rather than a wrong one.+- `notImplemented` now names the bounding-box limitation explicitly: on an L-shaped or slotted board, a part inside the box but outside the real outline is NOT caught.+ ## 0.1.23 — 2026-09-01 - **`altium_review_board`: design review over ONE board read.** Phase 2 of the plan, from the lint catalog. Five checks, all pure functions over data the bridge already reads — so adding them cost no new Altium API surface, which is the whole point: a wrong identifier in the agent halts the script engine behind a modal on the user's screen.   - `placeholder_values` (error) — a component still carrying Altium's default Comment. It reaches the BOM as "Comment".
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.23",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.23/adom-bridge-altium-v0.1.23.zip",-  "sha256": "45c55098e23334a1b3b98407c2835396a0c26d21db52a0a4a9ff98b87fc28dd7",-  "size": 122949,+  "version": "0.1.24",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.24/adom-bridge-altium-v0.1.24.zip",+  "sha256": "db394b52c27eb6c501c25dc2779b05133ec1d70ec587baf3be41b3a9ddf10870",+  "size": 124096,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.24.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.23",+  "version": "0.1.24",   "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.23",+  "version": "0.1.24",   "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.23+0.1.24
src/agent/AdomAgent.pas.tmpl+39−2
@@ -3716,6 +3716,41 @@ Begin     Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JStr('sections', Sects) + S + '}'; End; +{ The board's extent, for the checks that need to know where the edge is. Uses the SAME guarded+  pattern the library reader already relies on: a library's internal board has a NIL BoardOutline and+  touching BoundingRectangle on it halts the engine (measured 2026-08-30). Reports which source it+  used, because the outline's bounding box and the board's bounding box are not the same claim. }+Function CmdReadOutline(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    R     : TCoordRect;+    Src   : String;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then+    Begin+        Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}';+        Exit;+    End;+    If Board.BoardOutline <> Nil Then+    Begin+        R := Board.BoardOutline.BoundingRectangle;+        Src := 'boardOutline';+    End+    Else+    Begin+        R := Board.BoundingRectangle;+        Src := 'boardBounds';+    End;+    Result := '{' + JRaw('ok','true') + ',' + JStr('source', Src) + ',' ++        JRaw('leftMil', IntToStr(Round(CoordToMils(R.Left)))) + ',' ++        JRaw('bottomMil', IntToStr(Round(CoordToMils(R.Bottom)))) + ',' ++        JRaw('rightMil', IntToStr(Round(CoordToMils(R.Right)))) + ',' ++        JRaw('topMil', IntToStr(Round(CoordToMils(R.Top)))) + ',' ++        JRaw('widthMil', IntToStr(Round(CoordToMils(R.Right - R.Left)))) + ',' ++        JRaw('heightMil', IntToStr(Round(CoordToMils(R.Top - R.Bottom)))) + '}';+End;+ { ONE round trip to READ the whole board. Composed from the EXISTING readers on purpose: no new Altium   API surface, so it cannot introduce an undeclared identifier (which halts the engine behind a   modal). The bridge shapes these into typed views; recorded dumps are the unit-test fixtures. }@@ -3731,7 +3766,7 @@ Begin         Exit;     End;     Sects := Param(Lines, 'sections');-    If Sects = '' Then Sects := 'components,nets,layers,copper,rules,occupancy';+    If Sects = '' Then Sects := 'components,nets,layers,copper,rules,occupancy,outline';     S := '';     If Pos('components', Sects) > 0 Then S := S + ',' + JRaw('components', CmdComponentData(Lines));     If Pos('nets', Sects) > 0 Then S := S + ',' + JRaw('nets', CmdListNets(Lines));@@ -3739,6 +3774,7 @@ Begin     If Pos('copper', Sects) > 0 Then S := S + ',' + JRaw('copper', CmdCopperLayers(Lines));     If Pos('rules', Sects) > 0 Then S := S + ',' + JRaw('rules', CmdReadRules(Lines));     If Pos('occupancy', Sects) > 0 Then S := S + ',' + JRaw('occupancy', CmdLayerOccupancy(Lines));+    If Pos('outline', Sects) > 0 Then S := S + ',' + JRaw('outline', CmdReadOutline(Lines));     Result := '{' + JRaw('ok','true') + ',' + JStr('board', Board.FileName) + ',' + JStr('sections', Sects) + S + '}'; End; @@ -3753,6 +3789,7 @@ Begin     Else If Cmd = 'read_board' Then Result := CmdReadBoard(Lines)     Else If Cmd = 'layer_occupancy' Then Result := CmdLayerOccupancy(Lines)     Else If Cmd = 'read_schematic' Then Result := CmdReadSchematic(Lines)+    Else If Cmd = 'read_outline' Then Result := CmdReadOutline(Lines)     Else If Cmd = 'lib_list' Then Result := CmdLibList(Lines)     Else If Cmd = 'lib_select' Then Result := CmdLibSelect(Lines)     Else If Cmd = 'compile' Then Result := CmdCompile(Lines)@@ -3803,7 +3840,7 @@ Begin         gRunning := False;         Result := '{' + JRaw('ok', 'true') + ',' + JStr('note', 'agent loop stopping') + '}';     End-    Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives,io_bench,read_board,layer_occupancy,read_schematic') + '}';+    Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives,io_bench,read_board,layer_occupancy,read_schematic,read_outline') + '}'; End;  { ------------------------------------------------------------------ loop --- }
src/server.py+70−4
@@ -1567,13 +1567,79 @@ def _chk_duplicate_designators(board, sheet):     return out  ++def _outline_box(board):+    """The board extent, or None. `source` matters: 'boardOutline' is the real outline's bounding+    box; 'boardBounds' is everything on the board including stray primitives outside it, which is a+    much weaker claim and must not be used to say a part is off-board."""+    o = board.get("outline") or {}+    if not o.get("ok"):+        return None+    for k in ("leftMil", "rightMil", "bottomMil", "topMil"):+        if not isinstance(o.get(k), (int, float)):+            return None+    return o+++def _chk_components_outside_board(board, sheet):+    """error — a placed component sitting outside the board extent.++    NOTE this is a BOUNDING BOX test, not a polygon test: on an L-shaped or slotted board a part+    inside the bounding box but outside the real outline is NOT caught, and the message says so+    rather than implying more precision than the data carries."""+    box = _outline_box(board)+    if not box or box.get("source") != "boardOutline":+        return []+    l, r, b, tp = box["leftMil"], box["rightMil"], box["bottomMil"], box["topMil"]+    out = []+    for c in (board.get("components") or {}).get("components") or []:+        x, y = c.get("xMil"), c.get("yMil")+        if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):+            continue+        if x < l or x > r or y < b or y > tp:+            out.append({"check": "components_outside_board_outline", "severity": "error",+                        "designator": c.get("designator"),+                        "message": f"{c.get('designator')} sits at ({x}, {y}) mil, outside the board "+                                   f"bounding box ({l}..{r}, {b}..{tp} mil)."})+    return out+++def _chk_components_near_board_edge(board, sheet, margin_mil=20):+    """warning — a component origin within `margin` of the board edge.++    Named for what it measures: COMPONENT ORIGINS, not pads. A pad-to-edge check needs per-pad+    geometry the board read does not carry yet, and calling this 'pads_near_board_edge' would claim+    a precision it does not have."""+    box = _outline_box(board)+    if not box or box.get("source") != "boardOutline":+        return []+    l, r, b, tp = box["leftMil"], box["rightMil"], box["bottomMil"], box["topMil"]+    out = []+    for c in (board.get("components") or {}).get("components") or []:+        x, y = c.get("xMil"), c.get("yMil")+        if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):+            continue+        if x < l or x > r or y < b or y > tp:+            continue  # already reported as outside+        d = min(x - l, r - x, y - b, tp - y)+        if d < margin_mil:+            out.append({"check": "components_near_board_edge", "severity": "warning",+                        "designator": c.get("designator"),+                        "message": f"{c.get('designator')} origin is {int(d)} mil from the board edge "+                                   f"(under the {margin_mil} mil margin)."})+    return out++ BOARD_CHECKS = [_chk_placeholder_values, _chk_single_pin_nets, _chk_unused_mechanical_layers,-                _chk_off_grid_components, _chk_duplicate_designators]+                _chk_off_grid_components, _chk_duplicate_designators,+                _chk_components_outside_board, _chk_components_near_board_edge]  # Named so a clean result is not mistaken for a complete one. REVIEW_NOT_IMPLEMENTED = {-    "components_outside_board_outline": "needs the board outline, which read_board does not carry yet",-    "pads_near_board_edge": "needs the board outline",+    "pads_near_board_edge": "needs per-PAD geometry; components_near_board_edge measures component "+                            "origins instead, which is a weaker claim",+    "outline_shape_checks": "the outline read carries a BOUNDING BOX, not the polygon — a part inside "+                            "the box but outside an L-shaped or slotted outline is not caught",     "unrouted_connections": "needs the ratsnest",     "silkscreen_over_pad": "needs per-layer primitive geometry",     "via_antennas": "needs via/track topology",@@ -1590,7 +1656,7 @@ def h_review_board(args):     g = _need_licensed()     if g:         return g-    r = agent.call("read_board", {"sections": "components,nets,layers,copper,occupancy",+    r = agent.call("read_board", {"sections": "components,nets,layers,copper,occupancy,outline",                                   "limit": args.get("limit", 2000)},                    timeout=float(args.get("timeout", 90)))     board = r.get("data") or {}