docs(pid): track A shipped — CLAUDE.md, README, changelog with the live COM smoke record

scripts/smoke_pid_com.py runs the whole track against the live AutoCAD in
a new document. ComBackend.connect() is lazy by design, so the script reads
reachability from system_status()["connected"] rather than from connect()
as the plan sketched, and closes its document without saving unless --keep
is passed. On this machine (AutoCAD 2026, "25.1s (LMS Tech)") it exits 0:
5 nodes, 4 edges, 0 dangling, confidence_min 1.0, 0 critique issues,
2 XDATA chunks — identical to the headless engine on the same spec. The
first attempt exited 2 on a stale win32com gen_py cache (a package folder
holding only __pycache__); an environment fault, no backend change needed.

A/B against v1.5.1 published as ab-v1.5.1-vs-v1.6.0-dev.json: 26/29 ->
29/29, three miss -> pass, zero regressed. check_doc_numbers.py now names
the report and the two row labels in one place (AB_REPORT, AB_*_LABEL),
and README.md / benchmarks/README.md carry the new rows; the v1.5.0 and
v1.4.0 tables stay as history.

README: P&ID row, the showcase and catalogue images, the token table
re-measured (161 / 44,930 by default, 152 / 41,420 with TOOL_PACKS=core,
lean 50 / 14,007), every stale count reconciled (154 -> 166 registered,
149 -> 161 advertised, corpus 158/672 -> 162/731 by the same count, 19 ->
20 groups, 6 -> 8 resources, six-task matrix with pid_roundtrip), a
"Known limitations of the P&ID track" subsection, and the snapshot line at
this machine's 2423 collected tests (CI has not run on this tree; noted in
the changelog with the pre-existing 1369 vs 1363 drift).

CLAUDE.md: section 17, sections 5 and 7 counts, TOOL_PACKS, the two 1.6
contracts and the reader rules. Spec status -> implemented. The plan and
spec markdown were reformatted by ruff (fenced Python only) because
`ruff format --check .` was already red on them at HEAD and the gate
requires the whole tree clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-16 13:34:06 +03:00
co-authored by Claude Opus 5
parent 8d9c444b76
commit 2be5e4180d
9 changed files with 2604 additions and 789 deletions
+169
View File
@@ -5,6 +5,175 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
Track A of 1.6: the server becomes a P&ID expert and a P&ID reader. Nine
`pid_*` tools in a new SECTION 17, two generic contracts every engine
implements, six critique focuses that join `drawing_finalize`, and a
`TOOL_PACKS` gate so a mechanical-only client never pays for the P&ID
surface. Spec: `docs/superpowers/specs/2026-09-15-v1.6-pid-design.md`.
### Added
- **P&ID — 9 tools, pack `pid`.**
- `pid_symbol_list` / `pid_symbol_insert`: a 43-symbol ISO 10628-2 /
ISA-5.1 catalogue authored in code (11 valve bodies × 6 actuators, 6
rotating machines, 4 heat exchangers, 6 parametric vessels with nozzle
ports, 7 inline items, the instrument bubble in 4 types × 5 locations,
5 line markers, 3 connectors), placed as real blocks with a `TAG`
ATTRIB, named ports, and an `ACADMCP_PID` XDATA payload. Ports are
transformed through the INSERT's real rotation, scale and mirror; a
stretched symbol (`|x_scale| ≠ |y_scale|`) has no circle for a radial
port and is refused rather than answered with a wrong point.
- `pid_line_draw`: port-to-port orthogonal routing (`auto` picks from seven
candidates by fewest bends, then length; `direct`; or waypoints), eight
ISA-5.1 line classes mapped to layer / linetype / marker blocks, and line
numbers filled from a format (default
`{size}-{service}-{seq}-{spec}-{insulation}`; an empty field drops out
with its separator). A route that cannot honour the `stub` on both
ports is refused by name (`route((0,0), 0°, (3,8), 180°, stub=5)`
raises) rather than drawn short; a non-axis port direction, a NaN
coordinate and an unfillable format token are refused before anything
is written. Lines it crosses are **counted and
reported** (`crossings`), never silently drawn over — and never refused.
- `pid_tag_parse`: the ISA-5.1 identification-letter grammar (Table 4.1,
pinned by `tests/data/isa51_tags.json`), with the equipment-prefix map.
- `pid_graph`: the reader. Every symbol INSERT becomes a node with a
`source` and a `confidence` — catalogue name 1.0, `ACADMCP_PID` payload
0.95, `TAG`-style attribute or keyword block name 0.6 with inferred
ports, an INSERT a plain line merely touches 0.3. Confidence is the
minimum of its inputs, never raised by agreement between heuristics.
Each line end resolves to a port, a junction on another line, or
`dangling` with the nearest connectable port as a hint, always within
its own space (`scope="all"` never joins two layouts). Edge `length` is
the bulge-aware length the engine reports, not a chord walk (12.5 %
short on a semicircular jump).
- `pid_instrument_index` / `pid_line_list` / `pid_equipment_list`: derived
from the graph only, optionally written as CSV. An instrument tapping a
pipe reports the pipe's line number, never its own signal wire.
- `pid_from_spec`: a whole sheet from one JSON spec in one transaction —
rolled back entirely on the first refusal — returning the handles, the
graph and the critique; `dry_run=True` routes in memory and touches
nothing.
- **`block_define(name, entities, attdefs)`** on both engines: a block from
typed primitive specs (line, circle, arc, polyline, text, solid) and
ATTDEFs, no source entities needed; `overwrite=True` replaces the contents
so existing INSERTs keep the name. Validation runs before any write: a
DXF-illegal layer name, a duplicate or trailing-newline ATTDEF tag, a
multi-line or trailing-caret string are `TypeError`/`ValueError` naming
`entities[i]` / `attdefs[i]`, and nothing is created.
- **`entity_get_xdata` / `entity_set_xdata`** on both engines, typed by DXF
group code. The two limits AutoCAD enforces silently are enforced loudly
and before the write: a string over **255 characters** and an entity
budget of **16 KB across every application** (three 15 KB apps on one
entity used to pass headlessly and would have failed `SetXData` live).
LF/CR in a string is refused — it split a DXF tag, the saved file failed
to reopen and, because every undo snapshot is a DXF save,
`transaction_rollback` broke after one such write. `nan`/`inf` are refused
rather than written as literal text that reads back as `null`.
- **Six critique focuses** — `pid_dangling_line`, `pid_duplicate_tag`,
`pid_incompatible_connection`, `pid_untagged_instrument`,
`pid_illegal_tag`, `pid_unconnected_equipment` — run under `focus=None`
and inside `drawing_finalize`, over one cached graph. They score
evidence only: a drawing without a recognised P&ID node returns `[]`, a
0.3 touch guess can neither create nor suppress a finding, and a line
ending on a sheet frame (an INSERT the drawing does not vouch for) is
dangling. `drawing_refine` hints name only ports `pid_line_draw` accepts.
- **Two resources:** `autocad://pid/symbols` (the catalogue with ports and
parameter schemas) and `autocad://standards/isa51` (the letter tables and
the grammar's disambiguation rules).
- **Benchmark evidence.** `benchmarks/tasks_v4.py` adds `pid_roundtrip`
(the example sheet drawn through `pid_from_spec`, read back by a graph
builder that never sees the spec); the correctness suite grows to 29 with
three P&ID checks. A/B against `v1.5.1`: **26 / 29 → 29 / 29, three
`miss → pass`, zero regressed** (`ab-v1.5.1-vs-v1.6.0-dev.json`).
- `scripts/smoke_pid_com.py` runs the track once against the live AutoCAD
on the machine, in a new document; `scripts/render_pid_catalog.py` and
`scripts/render_readme_pid.py` rebuild the two README images.
### Fixed
- **Headless `block_insert(attributes=…)` lost its ATTRIBs.** The ezdxf path
used `add_auto_blockref`, which wraps the INSERT in an anonymous `*U`
block: the handle it returned reported `block_name` `*U1`, and
`block_get_attributes` on it returned `{}`. Reproduce on v1.5.1 with
`block_create_from_entities` of a block with one ATTDEF, then
`block_insert(name, attributes={"TAG": "P-101"})` and
`block_get_attributes(handle)`. It now inserts the named block with
`add_auto_attribs`, so the INSERT carries the name and the ATTRIBs, on
both engines alike. `block_insert` of an undefined name is refused before
writing (it used to return a handle whose dangling INSERT `drawing_audit`
later deleted).
- Live LWPOLYLINEs report as `POLYLINE` through ActiveX (`AcDbPolyline`),
which the P&ID line reader's type filter excluded: on AutoCAD every
auto-built line number restarted at seq 1 and `crossings` was always 0.
Both spellings are accepted; the COM `_entity_info` polyline branch now
returns `points` (and per-vertex `bulges`) for a live LWPOLYLINE.
- Polyline `bulges` now leave both engines in WCS: a mirrored polyline's
arcs were handed out in the entity frame next to WCS points, so a reader
tested them on the wrong side of the chord.
- INSERT properties gain `geometry_bbox` (drawn geometry only, ATTRIBs
excluded) and `mirrored` on both engines. The COM box is exact at right
angles and measured off them; a member ActiveX cannot measure is carried
by its corners and the box says `approximate: true`.
### Changed
- **`TOOL_PACKS=all|core,pid`** (default `all`) advertises only the vertical
packs a client uses; `core` is always on and unknown names are ignored
with a warning. It composes with `TOOL_PROFILE=lean` (which now carries
`pid_symbol_insert`, `pid_line_draw` and `pid_graph`: 50 tools, 47 with
`TOOL_PACKS=core`) and `ENABLE_3D`; `system_about` reports `tool_packs`.
Measured idle cost: 161 tools / 44,930 tokens by default, 152 / 41,420
with `TOOL_PACKS=core`.
- `PID_LAYERS` (`drawing_apply_iso_layers("pid")`) gains `PROCESS-LINE-TEXT`;
`drawing_plan` / `drawing_preflight` report `LAYER_SET_INTENT_MISMATCH`
when a P&ID intent is planned onto a non-`pid` layer set.
- The `prompt_pid_diagram` prompt template is rewritten onto the tools (`pid_from_spec`,
`pid_symbol_insert`, `pid_line_draw`, `pid_graph`) instead of describing
primitives.
- `CritiqueFocus` grows by the six `pid_*` values; benchmark matrix v4
(`--matrix v3` / `v2` reproduce the earlier sets); correctness suite 29.
- README release snapshot: 166 tools · 8 resources · 5 prompts · 2423
collected tests. The collected-test figure is this machine's; CI has not
run on this tree yet, and the pre-existing 1369-vs-1363 drift between the
README and Linux collection predates the track and is superseded by the
new figure.
### Live COM smoke
`scripts/smoke_pid_com.py`, AutoCAD 2026 (`25.1s (LMS Tech)`), Windows 11,
2026-09-16, in a new document (`Drawing2.dwg`) — `block_define` →
`pid_symbol_insert` → `entity_set_xdata` → `pid_line_draw` → `pid_graph` →
six focuses → `entity_get_xdata`, all through the COM engine, exit 0:
```json
{
"autocad": "25.1s (LMS Tech)",
"active_document": "Drawing2.dwg",
"handles": {"P-101": "98", "V-201": "A8", "FCV-101": "B8", "FIC-101": "C5", "OP-1": "CF"},
"graph_stats": {
"nodes_by_kind": {"connector": 1, "equipment": 2, "instrument": 1, "valve": 1},
"edges_by_class": {"electric": 1, "process_major": 3},
"dangling": 0,
"unclassified": 0,
"confidence_min": 1.0,
"entities_scanned": 16
},
"crossings_total": 0,
"critique_issues": 0,
"critique": [],
"xdata_chunks": 2
}
```
The headless engine reports the identical `graph_stats`, `crossings_total`,
`critique_issues` and `xdata_chunks` on the same spec. The first attempt
exited 2 on this machine because of a stale `win32com` `gen_py` cache for the
AutoCAD type library (a package directory holding only a `__pycache__`) — an
environment fault, not a backend one; deleting `%TEMP%\gen_py\3.11` cleared
it and no backend change was needed.
## [1.5.1] — 2026-08-06 ## [1.5.1] — 2026-08-06
A patch for three defects found *after* 1.5.0 went to PyPI, two of them by the A patch for three defects found *after* 1.5.0 went to PyPI, two of them by the
+11 -5
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
AutoCAD MCP Pro is a FastMCP 3.0 server that exposes ~154 tools, 6 resources, and 5 prompt templates for AutoCAD automation. (The exact tool count is reported dynamically by `system_status` / `system_about` — never hardcode it.) It runs with a dual-engine architecture: a live COM backend (Windows/AutoCAD required) and a headless ezdxf backend (works anywhere). AutoCAD MCP Pro is a FastMCP 3.0 server that exposes ~166 tools, 8 resources, and 5 prompt templates for AutoCAD automation. (The exact tool count is reported dynamically by `system_status` / `system_about` — never hardcode it.) It runs with a dual-engine architecture: a live COM backend (Windows/AutoCAD required) and a headless ezdxf backend (works anywhere).
## Running the Server ## Running the Server
@@ -76,9 +76,9 @@ Tools are organized into sections (counts are indicative — `system_about` is a
2. Entity Creation (14 tools): `entity_create_*` (includes `entity_create_table`, `leader_create_mleader`) 2. Entity Creation (14 tools): `entity_create_*` (includes `entity_create_table`, `leader_create_mleader`)
3. Dimensions (5 tools): `dimension_*` 3. Dimensions (5 tools): `dimension_*`
4. Entity Modification (16 tools): `entity_move/copy/rotate/scale/mirror/offset/delete/array_*`, corner ops (`entity_trim/extend/fillet/chamfer`), plus in-place `entity_edit_text` (TEXT/MTEXT content/height/rotation) and `entity_edit_geometry` (CIRCLE/LINE/ARC — center/radius/endpoints/angles), both handle-preserving 4. Entity Modification (16 tools): `entity_move/copy/rotate/scale/mirror/offset/delete/array_*`, corner ops (`entity_trim/extend/fillet/chamfer`), plus in-place `entity_edit_text` (TEXT/MTEXT content/height/rotation) and `entity_edit_geometry` (CIRCLE/LINE/ARC — center/radius/endpoints/angles), both handle-preserving
5. Entity Query (4 tools): `entity_get`, `entity_list`, `entity_delete_many`, `selection_get` 5. Entity Query (9 tools): `entity_get`, `entity_list`, `entity_delete_many`, `selection_get`, `selection_window/polygon/filter`, and `entity_get_xdata` / `entity_set_xdata` (typed by group code)
6. Layer Management (14 tools incl. 2 linetype_*): `layer_*`, `linetype_list`, `linetype_load` 6. Layer Management (14 tools incl. 2 linetype_*): `layer_*`, `linetype_list`, `linetype_load`
7. Block Operations (7 tools): `block_*` 7. Block Operations (8 tools): `block_*` — includes `block_define` (a block from typed primitive specs + ATTDEFs, no source entities needed)
8. Analysis & Query (8 tools): `analysis_*` — plus Batch (2), Templates (2), Validation (1) 8. Analysis & Query (8 tools): `analysis_*` — plus Batch (2), Templates (2), Validation (1)
9. View & Screenshot (4 tools — includes `view_zoom_and_screenshot`): `view_*` 9. View & Screenshot (4 tools — includes `view_zoom_and_screenshot`): `view_*`
10. Transactions (3 tools): `transaction_begin/commit/rollback` 10. Transactions (3 tools): `transaction_begin/commit/rollback`
@@ -88,8 +88,11 @@ Tools are organized into sections (counts are indicative — `system_about` is a
14. GD&T (ISO 1101 / ASME Y14.5): `gd_frame` (feature control frames), `datum_feature` — enforced by the `gdt` critique focus 14. GD&T (ISO 1101 / ASME Y14.5): `gd_frame` (feature control frames), `datum_feature` — enforced by the `gdt` critique focus
15. Layouts & Paper Space (12 tools): tab lifecycle `layout_list/create/set_current/delete/rename/copy`, viewports `viewport_create/list/set_scale/lock/delete`, and `entity_change_space` (CHSPACE — tagged `layout` *and* `modify`, and `layout` wins the group priority, so it files here). `drawing_export_pdf` takes an optional `layout` param and **does** project model content through viewports on both engines (`viewport_render` — v1.4 declared this COM-only and was wrong; the headless renderer only omits the viewport borders). `entity_change_space` is the mirror case — ezdxf-only (`chspace`), because ActiveX exposes no change-space member and the COM route is unverified. 15. Layouts & Paper Space (12 tools): tab lifecycle `layout_list/create/set_current/delete/rename/copy`, viewports `viewport_create/list/set_scale/lock/delete`, and `entity_change_space` (CHSPACE — tagged `layout` *and* `modify`, and `layout` wins the group priority, so it files here). `drawing_export_pdf` takes an optional `layout` param and **does** project model content through viewports on both engines (`viewport_render` — v1.4 declared this COM-only and was wrong; the headless renderer only omits the viewport borders). `entity_change_space` is the mirror case — ezdxf-only (`chspace`), because ActiveX exposes no change-space member and the COM route is unverified.
16. 3D Solids (5 tools, opt-in via `ENABLE_3D=true`, COM only): `solid_box/cylinder/extrude/revolve/boolean` — hidden from discovery and rejected while disabled; ezdxf reports `solid_3d` as unsupported (no headless ACIS). 16. 3D Solids (5 tools, opt-in via `ENABLE_3D=true`, COM only): `solid_box/cylinder/extrude/revolve/boolean` — hidden from discovery and rejected while disabled; ezdxf reports `solid_3d` as unsupported (no headless ACIS).
17. P&ID (9 tools, pack `pid`): `pid_symbol_list/insert` (ISO 10628-2 + ISA-5.1 catalogue authored in code, real blocks with TAG attributes, named ports, ACADMCP_PID XDATA), `pid_line_draw` (port-to-port orthogonal routing, ISA-5.1 line classes → layer/linetype/marker blocks, line numbers), `pid_tag_parse` (ISA-5.1 grammar), the reader `pid_graph` and its deliverables `pid_instrument_index/line_list/equipment_list`, and `pid_from_spec` (whole sheet, one transaction). Six `pid_*` critique focuses (`pid_dangling_line`, `pid_duplicate_tag`, `pid_incompatible_connection`, `pid_untagged_instrument`, `pid_illegal_tag`, `pid_unconnected_equipment`) run under `focus=None` and inside `drawing_finalize`; they return nothing on a drawing without P&ID nodes. Two resources (`autocad://pid/symbols`, `autocad://standards/isa51`) carry the catalogue and the ISA-5.1 letter tables. Both engines implement every P&ID contract; the COM paths were executed live once by `scripts/smoke_pid_com.py` (AutoCAD 2026).
**Tool profiles:** `TOOL_PROFILE=lean|full` (default `full`) controls the advertised surface — `lean` ≈ 47 curated drafting tools (including `cad_batch`, since a client with a tight tool cap is exactly the client paying most per turn). Applied in the lifespan; reported by `system_about`. The `core` profile was removed in v1.5.0 (the discovery layer replaced it); `TOOL_PROFILE=core` falls back to `full` with a warning. **Tool profiles:** `TOOL_PROFILE=lean|full` (default `full`) controls the advertised surface — `lean` ≈ 50 curated drafting tools (47 with `TOOL_PACKS=core`; including `cad_batch`, since a client with a tight tool cap is exactly the client paying most per turn). Applied in the lifespan; reported by `system_about`. The `core` profile was removed in v1.5.0 (the discovery layer replaced it); `TOOL_PROFILE=core` falls back to `full` with a warning.
**Tool packs:** `TOOL_PACKS=all|core,pid` (default `all`) advertises only the vertical packs a client uses; `core` is always on and is everything no other pack claims; unknown names are ignored with a warning; a `lean` profile intersects with the enabled packs. `system_about` reports `tool_packs`.
**Tool discovery:** `DISCOVERY_MODE=off|search` (default `off`). `search` replaces the advertised catalog with `search_tools` + `call_tool`, ranking hits over an AutoCAD command / synonym corpus (`discovery/aliases.py`) layered on fastmcp's BM25 index. **Tool discovery:** `DISCOVERY_MODE=off|search` (default `off`). `search` replaces the advertised catalog with `search_tools` + `call_tool`, ranking hits over an AutoCAD command / synonym corpus (`discovery/aliases.py`) layered on fastmcp's BM25 index.
@@ -101,7 +104,7 @@ Tools are organized into sections (counts are indicative — `system_about` is a
**Measuring:** never read vertices back and shoelace them — that loses 28.2% of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry, and its payload states its own accuracy: `exact`, `flatten_tolerance`, `assumed_closed`, `self_intersecting`. A HATCH reports the area it *fills* (outer loops minus islands) plus `hatch_style`; on a curved hatch edge `flatten_tolerance` is not the accuracy knob, because ezdxf hands boundaries over as cubic Beziers whose ~0.028% circle error is already baked in. `boundary_trace` / `boundary_from_entities` report the same number `analysis_measure_entity` gives for the polyline they just drew — if those ever diverge, one of them is approximating and neither says so. **Measuring:** never read vertices back and shoelace them — that loses 28.2% of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry, and its payload states its own accuracy: `exact`, `flatten_tolerance`, `assumed_closed`, `self_intersecting`. A HATCH reports the area it *fills* (outer loops minus islands) plus `hatch_style`; on a curved hatch edge `flatten_tolerance` is not the accuracy knob, because ezdxf hands boundaries over as cubic Beziers whose ~0.028% circle error is already baked in. `boundary_trace` / `boundary_from_entities` report the same number `analysis_measure_entity` gives for the polyline they just drew — if those ever diverge, one of them is approximating and neither says so.
**Benchmark matrix:** `benchmarks/tasks_v4.py` is the current set (16 tasks; `--matrix v3` / `--matrix v2` reproduce the earlier sets). The published competitor reports are pinned v1.4 runs against v2 and are **never** back-filled for the five v3 tasks or the v4 `pid_roundtrip` — the chart shows them `not run`, not zero. `benchmarks/correctness_suite.py` (29 checks) is the release A/B gate: `python benchmarks/compare_versions.py v1.4.0`. **Benchmark matrix:** `benchmarks/tasks_v4.py` is the current set (16 tasks; `--matrix v3` / `--matrix v2` reproduce the earlier sets). The published competitor reports are pinned v1.4 runs against v2 and are **never** back-filled for the five v3 tasks or the v4 `pid_roundtrip` — the chart shows them `not run`, not zero. `benchmarks/correctness_suite.py` (29 checks) is the release A/B gate: `python benchmarks/compare_versions.py v1.5.1` (the published report is `benchmarks/results/published/ab-v1.5.1-vs-v1.6.0-dev.json`; `scripts/check_doc_numbers.py` names it in `AB_REPORT`).
### Adding a New Tool ### Adding a New Tool
@@ -126,6 +129,9 @@ Tools are organized into sections (counts are indicative — `system_about` is a
- **Screenshot**: COM uses Win32 window capture (Pillow required); ezdxf renders via matplotlib. - **Screenshot**: COM uses Win32 window capture (Pillow required); ezdxf renders via matplotlib.
- **`_dc(obj)`**: converts dataclasses to dicts recursively for JSON serialization. - **`_dc(obj)`**: converts dataclasses to dicts recursively for JSON serialization.
- **Engineering layer scaffold**: `drawing_new` auto-bootstraps standard linetypes (CENTER, HIDDEN, PHANTOM) and engineering layers (GEOMETRY, DIM, CENTER, HIDDEN, PHANTOM, HATCH, TEXT, TITLEBLOCK). Pass `bootstrap=False` to opt out. - **Engineering layer scaffold**: `drawing_new` auto-bootstraps standard linetypes (CENTER, HIDDEN, PHANTOM) and engineering layers (GEOMETRY, DIM, CENTER, HIDDEN, PHANTOM, HATCH, TEXT, TITLEBLOCK). Pass `bootstrap=False` to opt out.
- **Generic contracts added in 1.6:** `block_define(name, entities, attdefs)` (typed primitives + ATTDEFs, both engines; `overwrite` replaces contents so INSERTs keep the name) and `entity_get/set_xdata` (typed by group code; 255-char strings, 16 KB per entity, enforced before writing).
- **P&ID reader rules:** `pid_graph` never modifies the drawing, reads geometry from the drawing (an INSERT's real rotation/scale) rather than the catalogue, and reports `source`/`confidence` per node (catalog 1.0, xdata 0.95, heuristic 0.6, inferred 0.3) — never raised by agreement between heuristics. Read `stats.confidence_min` before trusting a foreign drawing.
- **`TOOL_PACKS=all|core,pid`** advertises only the packs a client uses; `core` is always on; `system_about` reports `tool_packs`.
- **Production drawings**: For real engineering output, use the `engineering/` package primitives via the `gear_*` / `keyway_*` / `titleblock_*` MCP tools — do NOT hand-draw teeth/keyways/sections with raw `entity_create_*` calls. Always end with `drawing_finalize` for the 8-step validator. - **Production drawings**: For real engineering output, use the `engineering/` package primitives via the `gear_*` / `keyway_*` / `titleblock_*` MCP tools — do NOT hand-draw teeth/keyways/sections with raw `entity_create_*` calls. Always end with `drawing_finalize` for the 8-step validator.
### Premium Drawing Rules ### Premium Drawing Rules
+33 -14
View File
@@ -17,22 +17,31 @@ Live through COM on Windows, or headless through ezdxf anywhere — one typed co
<sub><b>Not a mockup.</b> Every line on this sheet was drawn by the tools this server exposes — ISO layers, involute gear geometry, DIN 6885 keyway, section A-A, ISO 129 dimensions, an ISO 286 <code>H7</code> bore fit, ISO 7200 title block — then rendered headlessly by <code>view_screenshot</code>. <code>drawing_critique</code> returns <b>0 issues</b> on it. Rebuild it with <code>python scripts/render_readme_showcase.py</code>.</sub> <sub><b>Not a mockup.</b> Every line on this sheet was drawn by the tools this server exposes — ISO layers, involute gear geometry, DIN 6885 keyway, section A-A, ISO 129 dimensions, an ISO 286 <code>H7</code> bore fit, ISO 7200 title block — then rendered headlessly by <code>view_screenshot</code>. <code>drawing_critique</code> returns <b>0 issues</b> on it. Rebuild it with <code>python scripts/render_readme_showcase.py</code>.</sub>
<img src="https://raw.githubusercontent.com/U-C4N/Autocad-MCP/main/docs/assets/pid-showcase.png" alt="P&ID sheet: a centrifugal pump, a diaphragm-actuated globe control valve, a vertical vessel with two nozzles, a flow controller bubble and an off-page connector, joined by numbered process lines and an electric signal line" width="800">
<sub><b>The same server as a P&ID drafter.</b> <code>pid_from_spec</code> placed five catalogue symbols as real blocks with <code>TAG</code> attributes and named ports, routed four port-to-port lines with ISA-5.1 classes and line numbers in one transaction, then <code>pid_graph</code> read the sheet back: 5 nodes, 4 edges, 0 dangling ends, <code>confidence_min</code> 1.0. Rebuild it with <code>python scripts/render_readme_pid.py</code>.</sub>
<img src="https://raw.githubusercontent.com/U-C4N/Autocad-MCP/main/docs/assets/pid-catalog.png" alt="Contact sheet of the 43 P&ID catalogue symbols: valve bodies with actuators, pumps and compressors, heat exchangers, vessels, instrument bubbles, line markers and connectors" width="800">
<sub><b>The catalogue, authored in code.</b> 43 ISO 10628-2 / ISA-5.1 symbols — 11 valve bodies × 6 actuators, 6 rotating machines, 4 heat exchangers, 6 parametric vessels, 7 inline items, the instrument bubble in 4 types × 5 locations, 5 line markers, 3 connectors — every one placed through <code>pid_symbol_insert</code>'s own path. Rebuild it with <code>python scripts/render_pid_catalog.py</code>.</sub>
</div> </div>
> **v1.5 release snapshot:** 166 tools · 8 resources · 5 prompt templates · 1369 collected tests. > **v1.5 release snapshot:** 166 tools · 8 resources · 5 prompt templates · 2423 collected tests.
> 154 is the **registered** count; a default install advertises 149 over `tools/list`, > 166 is the **registered** count; a default install advertises 161 over `tools/list`,
> because `ENABLE_3D` is unset. `system_about` is the runtime authority. > because `ENABLE_3D` is unset. `system_about` is the runtime authority.
## Why this exists ## Why this exists
**A big MCP server is expensive to be connected to.** The full catalog costs a client **40,305 tokens** before it has asked for anything. Discovery mode replaces it with two tools and costs **356**. **A big MCP server is expensive to be connected to.** The full catalog costs a client **44,930 tokens** before it has asked for anything. Discovery mode replaces it with two tools and costs **356**.
**A drafter searches for `FILLET`, not `entity_fillet`.** Those command names appeared in no tool name or description — `df = 0` against a stock index, not badly ranked but *absent*. The fix was data: an authored corpus of **158 AutoCAD command names and 672 synonym phrases** covering all 154 tools. A test refuses to let a tool exist without one. **A drafter searches for `FILLET`, not `entity_fillet`.** Those command names appeared in no tool name or description — `df = 0` against a stock index, not badly ranked but *absent*. The fix was data: an authored corpus of **162 AutoCAD command names and 731 synonym phrases** covering all 166 tools. A test refuses to let a tool exist without one.
| Advertised surface | Tools seen | Idle cost | | Advertised surface | Tools seen | Idle cost |
|---|---:|---:| |---|---:|---:|
| `TOOL_PROFILE=full` (default) | 149 | 40,305 tokens | | `TOOL_PROFILE=full` (default) | 161 | 44,930 tokens |
| `TOOL_PROFILE=lean` | 47 | 12,131 tokens | | `TOOL_PACKS=core` (full profile) | 152 | 41,420 tokens |
| `TOOL_PROFILE=lean` | 50 | 14,007 tokens |
| `DISCOVERY_MODE=search` | 2 | **356 tokens** | | `DISCOVERY_MODE=search` | 2 | **356 tokens** |
> [!NOTE] > [!NOTE]
@@ -91,7 +100,8 @@ Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp --transpor
| Geometry | lines, arcs, polylines, splines, hatches, trim/extend/fillet/chamfer, handle-preserving edits | | Geometry | lines, arcs, polylines, splines, hatches, trim/extend/fillet/chamfer, handle-preserving edits |
| Annotation | ISO 129 toleranced dimensions, ISO 286 fits (`fit="H7"`), TABLE, MLEADER, GD&T frames and datums (ISO 1101) | | Annotation | ISO 129 toleranced dimensions, ISO 286 fits (`fit="H7"`), TABLE, MLEADER, GD&T frames and datums (ISO 1101) |
| Engineering generators | involute gears (front + section A-A), DIN 6885 keyed bores, ISO A3 title block | | Engineering generators | involute gears (front + section A-A), DIN 6885 keyed bores, ISO A3 title block |
| Discovery | `search_tools` ranked over an AutoCAD command and synonym corpus — `FILLET`, `BPOLY`, `QSELECT`, `WBLOCK`, `OVERKILL`, `CHSPACE` each rank **#1** of the 149-tool advertised catalog | | P&ID | catalogue blocks with ports and tags (ISO 10628-2 / ISA-5.1), port-to-port lines with ISA-5.1 classes and line numbers, `pid_graph` reads any P&ID back with confidence, instrument index / line list / equipment list, `pid_from_spec` one-call sheets |
| Discovery | `search_tools` ranked over an AutoCAD command and synonym corpus — `FILLET`, `BPOLY`, `QSELECT`, `WBLOCK`, `OVERKILL`, `CHSPACE` each rank **#1** of the 161-tool advertised catalog |
| Batching | `cad_batch` runs a step list in one round trip; `fields=` projects 11 result-heavy tools | | Batching | `cad_batch` runs a step list in one round trip; `fields=` projects 11 result-heavy tools |
| Paper space | tab lifecycle, viewports, `entity_change_space` (CHSPACE), `drawing_export_pdf(layout=…)` | | Paper space | tab lifecycle, viewports, `entity_change_space` (CHSPACE), `drawing_export_pdf(layout=…)` |
| Selection | window vs crossing stated back to the caller; a polygon tested against its own shape, not its bounding box | | Selection | window vs crossing stated back to the caller; a polygon tested against its own shape, not its bounding box |
@@ -103,7 +113,7 @@ Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp --transpor
| Quality loop | `drawing_preflight` → `drawing_plan` → `drawing_critique` → `drawing_refine` → `drawing_finalize` (0–100 score) | | Quality loop | `drawing_preflight` → `drawing_plan` → `drawing_critique` → `drawing_refine` → `drawing_finalize` (0–100 score) |
| Delivery | `drawing_deliver`: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks | | Delivery | `drawing_deliver`: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks |
<sub>154 tools in 19 groups. Plus 6 resources that cost nothing in the tool budget (<code>autocad://drawing/info</code>, <code>layers</code>, <code>blocks</code>, <code>entities/stats</code>, <code>entities/{layer_name}</code>, <code>system/status</code>) and 5 prompt templates.</sub> <sub>166 tools in 20 groups; <code>TOOL_PACKS=core</code> hides the nine <code>pid_*</code> tools from a client that never draws a P&ID. Plus 8 resources that cost nothing in the tool budget (<code>autocad://drawing/info</code>, <code>layers</code>, <code>blocks</code>, <code>entities/stats</code>, <code>entities/{layer_name}</code>, <code>system/status</code>, <code>pid/symbols</code>, <code>standards/isa51</code>) and 5 prompt templates.</sub>
**Two rules worth knowing.** Every coordinate in and out of a tool is WCS on both engines — the one exception is TEXT `rotation`, which stays in the entity frame because a mirrored TEXT is mirror-imaged and no scalar angle expresses that. And never read vertices back and shoelace them: that loses **28.2%** of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry and states its own accuracy. **Two rules worth knowing.** Every coordinate in and out of a tool is WCS on both engines — the one exception is TEXT `rotation`, which stays in the entity frame because a mirrored TEXT is mirror-imaged and no scalar angle expresses that. And never read vertices back and shoelace them: that loses **28.2%** of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry and states its own accuracy.
@@ -147,22 +157,23 @@ Self-measurement, produced by scripts in [`benchmarks/`](https://github.com/U-C4
| Version | Checks passing | Pass rate | Fixed | Regressed | | Version | Checks passing | Pass rate | Fixed | Regressed |
|---|---:|---:|---:|---:| |---|---:|---:|---:|---:|
| v1.5.0 *(baseline)* | 24 / 26 | 92.3 % | — | — | | v1.5.1 *(baseline)* | 26 / 29 | 89.7 % | — | — |
| **v1.5.1** *(this release)* | **26 / 26** | **100 %** | 2 | **0** | | **v1.6.0-dev** *(this branch)* | **29 / 29** | **100 %** | 3 | **0** |
Against the older `v1.4.0` baseline the same suite reports **21 / 26 → 26 / 26, five fixed, zero regressed**. Three of the five are new capability (`miss → pass`); two are repaired defects (`fail → pass`) — the diameter and radius callouts, which measured the leader as geometry and dimensioned a 40 mm bore as 60 at default settings. The three fixed rows are the P&ID checks, new capability (`miss → pass`): `block_define` round-trips an ATTDEF, `pid_tag_parse` reads `FIC-101`, and `pid_graph` counts the example sheet's four edges. The 26 checks v1.5.1 was gated on all still pass. Against the older `v1.4.0` baseline the same 26 reported **21 / 26 → 26 / 26, five fixed, zero regressed** — two of those were repaired defects (`fail → pass`), the diameter and radius callouts, which measured the leader as geometry and dimensioned a 40 mm bore as 60 at default settings.
### The task matrix — five tasks that can fail ### The task matrix — six tasks that can fail
An earlier matrix scored this server 10/10, which carried no information: every task in it exercised something the server was built around. Five were added because they *can* fail, and three did while being written. An earlier matrix scored this server 10/10, which carried no information: every task in it exercised something the server was built around. Five were added in 1.5 because they *can* fail, and three did while being written; 1.6 adds a sixth that the P&ID reader can fail on its own.
| Task | Verified against | | Task | Verified against |
|---|---| |---|---|
| `tool_discovery` | six AutoCAD command names, each ranking #1 | | `tool_discovery` | six AutoCAD command names, each ranking #1 |
| `token_budget` | 40,305 → 356 tokens, against a ceiling fixed in advance | | `token_budget` | 44,930 → 356 tokens, against a ceiling fixed in advance |
| `hatch_islands` | 300 filled with the island, 400 ignoring it | | `hatch_islands` | 300 filled with the island, 400 ignoring it |
| `selection_filter` | window 1, crossing 2, bounding box 3, polygon 1 | | `selection_filter` | window 1, crossing 2, bounding box 3, polygon 1 |
| `measure_from_handle` | 139.2699 against the 100.0 a vertex shoelace gives | | `measure_from_handle` | 139.2699 against the 100.0 a vertex shoelace gives |
| `pid_roundtrip` | the example sheet drawn by `pid_from_spec`, read back by `pid_graph`, which never sees the spec: 5 nodes, 4 edges, 0 dangling, `confidence_min` 1.0, `FIC-101` wired to `FCV-101` |
### Headless performance ### Headless performance
@@ -184,6 +195,14 @@ A page that only lists strengths is a page that has not been measured.
**Non-AutoCAD ProgIDs are unverified.** `CAD_PROGID` changes which COM application the backend attaches to; nothing beyond the connection has been tested against BricsCAD, ZWCAD or GstarCAD. **Non-AutoCAD ProgIDs are unverified.** `CAD_PROGID` changes which COM application the backend attaches to; nothing beyond the connection has been tested against BricsCAD, ZWCAD or GstarCAD.
### Known limitations of the P&ID track
**The router is orthogonal and blind.** `pid_line_draw` picks `auto` from seven orthogonal candidates — straight, one bend, two bends through the midline, or a stub out of each port joined by three bends — by fewest bends then length (or takes `direct`, or the waypoints you pass), and *counts* the lines it crosses — `crossings` comes back in every result and `pid_from_spec` sums it — but it does not route around them. Obstacle-avoiding routing is out of 1.6; move the symbol or pass waypoints.
**A foreign drawing is read at the confidence it earns, and no higher.** A block this server did not place classifies by its `TAG`-style attribute or a keyword in its name at 0.6 with inferred ports, or at 0.3 when a line merely touches it — and a 0.3 guess can neither raise nor suppress a critique finding. On the live engine an INSERT turned off a right angle whose members ActiveX cannot measure reports an *approximate* box; `pid_graph` takes the tighter wall of that and the attribute-inclusive box on every side, but a line ending on such a body's true edge can still be reported `dangling` with the nearest port as the hint. Read `stats.confidence_min` first.
**Same-file only, DXF headlessly.** Off-page connectors link within one drawing; links across files are not resolved. The headless engine reads DXF; a DWG P&ID is read through the live backend. No DEXPI/Proteus export and no ISA-5.2 binary-logic symbols in this release, recorded in the spec so nobody re-derives the cut.
## Configuration ## Configuration
Nothing loads a `.env` file — export these, or set them in your MCP client's `env` block. Nothing loads a `.env` file — export these, or set them in your MCP client's `env` block.
+23 -7
View File
@@ -214,29 +214,45 @@ python benchmarks/compare_versions.py v1.0.0 # vs a tag/ref
python benchmarks/compare_versions.py --json results.json python benchmarks/compare_versions.py --json results.json
``` ```
### Result — v1.5.1 vs v1.5.0 (release gate) ### Result — this branch (1.6.0-dev) vs v1.5.1 (release gate)
29 checks, ezdxf backend, one subprocess per check. Machine-readable report: 29 checks, ezdxf backend, one subprocess per check. Machine-readable report:
[`results/published/ab-v1.5.1-vs-v1.6.0-dev.json`](results/published/ab-v1.5.1-vs-v1.6.0-dev.json).
| Version | Checks passing | Pass rate | Fixed | Regressed |
|---------|----------------|-----------|-------|-----------|
| **v1.5.1** (baseline) | 26 / 29 | 89.7 % | — | — |
| **v1.6.0-dev** (this branch) | 29 / 29 | 100 % | 3 | **0** |
The three are the P&ID checks Track A added — `pid_block_define_attdef_roundtrip`,
`pid_tag_parse_fic`, `pid_graph_edge_count` — all `miss → pass`: v1.5.1 has
none of the methods. Every one of the 26 checks it was released on still
passes, so the track added capability without moving a number it had already
earned.
### Result — v1.5.1 vs v1.5.0
26 checks at the time, ezdxf backend, one subprocess per check. Machine-readable report:
[`results/published/ab-v1.5.0-vs-v1.5.1.json`](results/published/ab-v1.5.0-vs-v1.5.1.json). [`results/published/ab-v1.5.0-vs-v1.5.1.json`](results/published/ab-v1.5.0-vs-v1.5.1.json).
| Version | Checks passing | Pass rate | Fixed | Regressed | | Version | Checks passing | Pass rate | Fixed | Regressed |
|---------|----------------|-----------|-------|-----------| |---------|----------------|-----------|-------|-----------|
| **v1.5.0** (baseline) | 24 / 26 | 92.3 % | — | — | | v1.5.0 (baseline) | 24 / 26 | 92.3 % | — | — |
| **v1.5.1** (this release) | 26 / 26 | 100 % | 2 | **0** | | v1.5.1 (that release) | 26 / 26 | 100 % | 2 | **0** |
v1.5.0 fails `diameter_dim_measures_the_diameter` and v1.5.0 fails `diameter_dim_measures_the_diameter` and
`radius_dim_ignores_the_leader_length`: it has both methods and gets both `radius_dim_ignores_the_leader_length`: it has both methods and gets both
wrong. That is the defect 1.5.1 exists to fix and the reason 1.5.0 is yanked. wrong. That is the defect 1.5.1 exists to fix and the reason 1.5.0 is yanked.
### Result — this release vs v1.4.0 ### Result — v1.5.1 vs v1.4.0
Same suite, older baseline: Same 26-check suite, older baseline:
[`results/published/ab-v1.4.0-vs-v1.5.1.json`](results/published/ab-v1.4.0-vs-v1.5.1.json). [`results/published/ab-v1.4.0-vs-v1.5.1.json`](results/published/ab-v1.4.0-vs-v1.5.1.json).
| Version | Checks passing | Pass rate | Fixed | Regressed | | Version | Checks passing | Pass rate | Fixed | Regressed |
|---------|----------------|-----------|-------|-----------| |---------|----------------|-----------|-------|-----------|
| **v1.4.0** (baseline) | 21 / 26 | 80.8 % | — | — | | v1.4.0 (baseline) | 21 / 26 | 80.8 % | — | — |
| **v1.5.1** (this release) | 26 / 26 | 100 % | 5 | **0** | | v1.5.1 (that release) | 26 / 26 | 100 % | 5 | **0** |
> This row used to read `v1.5.0 | 24 / 26 | 92.3 % | 5`, which was impossible on > This row used to read `v1.5.0 | 24 / 26 | 92.3 % | 5`, which was impossible on
> its face — 21 passing plus 5 fixed is 26, not 24 — and it contradicted the > its face — 21 passing plus 5 fixed is 26, not 24 — and it contradicted the
@@ -0,0 +1,188 @@
{
"summary": {
"baseline_ref": "v1.5.1",
"total": 29,
"old_pass": 26,
"new_pass": 29,
"old_pct": 89.7,
"new_pct": 100.0,
"fixed": 3,
"regressed": 0
},
"rows": [
{
"check": "core_line_length",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_circle_radius",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_layer_create",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_polyline_closed",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_linear_dim",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "core_save_dxf_roundtrip",
"category": "Core",
"old": "pass",
"new": "pass"
},
{
"check": "dim_aligned_no_error",
"category": "Dimensions",
"old": "pass",
"new": "pass"
},
{
"check": "dim_angular_no_error",
"category": "Dimensions",
"old": "pass",
"new": "pass"
},
{
"check": "array_polar_360_distinct",
"category": "Modify",
"old": "pass",
"new": "pass"
},
{
"check": "point_intersection_line_line",
"category": "Geometry",
"old": "pass",
"new": "pass"
},
{
"check": "point_tangent_external",
"category": "Geometry",
"old": "pass",
"new": "pass"
},
{
"check": "arc_has_length",
"category": "Query",
"old": "pass",
"new": "pass"
},
{
"check": "arc_select_by_length",
"category": "Query",
"old": "pass",
"new": "pass"
},
{
"check": "ezdxf_bounding_box",
"category": "Query",
"old": "pass",
"new": "pass"
},
{
"check": "mtext_rotation_roundtrip",
"category": "Entities",
"old": "pass",
"new": "pass"
},
{
"check": "screenshot_png",
"category": "Render",
"old": "pass",
"new": "pass"
},
{
"check": "dim_overlap_critique_fires",
"category": "Quality gate",
"old": "pass",
"new": "pass"
},
{
"check": "iso13567_dim_layer",
"category": "Quality gate",
"old": "pass",
"new": "pass"
},
{
"check": "construction_left_iso_caught",
"category": "Quality gate",
"old": "pass",
"new": "pass"
},
{
"check": "gear_no_self_overlap",
"category": "Engineering",
"old": "pass",
"new": "pass"
},
{
"check": "center_linetype_applied",
"category": "Entities",
"old": "pass",
"new": "pass"
},
{
"check": "hatch_area_subtracts_its_island",
"category": "Measurement",
"old": "pass",
"new": "pass"
},
{
"check": "boundary_area_agrees_with_measure",
"category": "Measurement",
"old": "pass",
"new": "pass"
},
{
"check": "two_vertex_circle_has_area",
"category": "Measurement",
"old": "pass",
"new": "pass"
},
{
"check": "diameter_dim_measures_the_diameter",
"category": "Dimensions",
"old": "pass",
"new": "pass"
},
{
"check": "radius_dim_ignores_the_leader_length",
"category": "Dimensions",
"old": "pass",
"new": "pass"
},
{
"check": "pid_block_define_attdef_roundtrip",
"category": "P&ID",
"old": "miss",
"new": "pass"
},
{
"check": "pid_tag_parse_fic",
"category": "P&ID",
"old": "miss",
"new": "pass"
},
{
"check": "pid_graph_edge_count",
"category": "P&ID",
"old": "miss",
"new": "pass"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
# v1.6 Track A — P&ID expert and reader # v1.6 Track A — P&ID expert and reader
**Date:** 2026-09-15 · **Status:** approved; implementation plan at **Date:** 2026-09-15 · **Status:** implemented 2026-09-16 (21 tasks of
`docs/superpowers/plans/2026-09-15-v1.6-pid-implementation.md` · **Parent:** `2026-09-15-v1.6-roadmap-design.md` (track A) · `docs/superpowers/plans/2026-09-15-v1.6-pid-implementation.md`; live COM smoke on AutoCAD 2026 recorded in `CHANGELOG.md` Unreleased; A/B vs v1.5.1 29 / 29) · **Parent:** `2026-09-15-v1.6-roadmap-design.md` (track A) ·
**Baseline:** v1.5.1 **Baseline:** v1.5.1
## 1. Summary ## 1. Summary
@@ -111,10 +111,13 @@ class Port:
kind: Literal["process", "signal"] kind: Literal["process", "signal"]
radius: float = 0.0 # radial ports only (bubbles): line starts on the circle radius: float = 0.0 # radial ports only (bubbles): line starts on the circle
@dataclass(frozen=True) @dataclass(frozen=True)
class SymbolSpec: class SymbolSpec:
name: str # block name, see 4.5 name: str # block name, see 4.5
family: Literal["valve", "rotating", "vessel", "heat", "misc", "instrument", "connector", "marker"] family: Literal[
"valve", "rotating", "vessel", "heat", "misc", "instrument", "connector", "marker"
]
primitives: tuple[dict, ...] # block_define entity specs (section 5.1) primitives: tuple[dict, ...] # block_define entity specs (section 5.1)
attdefs: tuple[dict, ...] # block_define attdef specs attdefs: tuple[dict, ...] # block_define attdef specs
ports: tuple[Port, ...] ports: tuple[Port, ...]
@@ -253,6 +256,7 @@ cost `mutate`, aliases `acad=("BLOCK", "BEDIT", "ATTDEF")`.
async def entity_get_xdata(self, handle: str, app_name: str | None = None) -> dict: async def entity_get_xdata(self, handle: str, app_name: str | None = None) -> dict:
"""→ {"handle", "xdata": {app: [value, ...]}, "backend"}; unknown app → {} not an error""" """→ {"handle", "xdata": {app: [value, ...]}, "backend"}; unknown app → {} not an error"""
@abstractmethod @abstractmethod
async def entity_set_xdata(self, handle: str, app_name: str, values: list) -> dict: async def entity_set_xdata(self, handle: str, app_name: str, values: list) -> dict:
"""→ {"ok": True, "handle", "app_name", "value_count", "bytes", "backend"}""" """→ {"ok": True, "handle", "app_name", "value_count", "bytes", "backend"}"""
+29 -11
View File
@@ -27,6 +27,12 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
#: The published A/B report the README and benchmarks/README quote. Bump this,
#: the two row labels below and the prose together, or the gate reports drift.
AB_REPORT = "benchmarks/results/published/ab-v1.5.1-vs-v1.6.0-dev.json"
AB_BASELINE_LABEL = r"v1\.5\.1"
AB_CURRENT_LABEL = r"v1\.6\.0-dev"
def _read(relative: str) -> str: def _read(relative: str) -> str:
return (ROOT / relative).read_text(encoding="utf-8") return (ROOT / relative).read_text(encoding="utf-8")
@@ -66,7 +72,7 @@ def derived() -> dict[str, object]:
"""Every number a document is allowed to quote, computed from the tree.""" """Every number a document is allowed to quote, computed from the tree."""
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
inventory = _json("docs/tool-inventory.json")["totals"] inventory = _json("docs/tool-inventory.json")["totals"]
ab = _json("benchmarks/results/published/ab-v1.5.0-vs-v1.5.1.json")["summary"] ab = _json(AB_REPORT)["summary"]
perf = { perf = {
w["workload_id"]: w["wall_ms"] w["workload_id"]: w["wall_ms"]
for w in _json("benchmarks/results/published/perf-ezdxf.json")["workloads"] for w in _json("benchmarks/results/published/perf-ezdxf.json")["workloads"]
@@ -81,6 +87,7 @@ def derived() -> dict[str, object]:
"resources": inventory["resources"], "resources": inventory["resources"],
"prompts": inventory["prompts"], "prompts": inventory["prompts"],
"groups": inventory["groups"], "groups": inventory["groups"],
"ab_baseline_ref": ab["baseline_ref"],
"ab_total": ab["total"], "ab_total": ab["total"],
"ab_old_pass": ab["old_pass"], "ab_old_pass": ab["old_pass"],
"ab_old_pct": ab["old_pct"], "ab_old_pct": ab["old_pct"],
@@ -112,19 +119,29 @@ CHECKS: list[tuple[str, str, str, str]] = [
"prompts", "prompts",
), ),
("README.md", "A/B suite size", r"(\d+) deterministic headless checks", "ab_total"), ("README.md", "A/B suite size", r"(\d+) deterministic headless checks", "ab_total"),
("README.md", "A/B baseline pass", r"v1\.5\.0 \*\(baseline\)\* \| (\d+) / \d+", "ab_old_pass"), (
("README.md", "A/B suite denominator", r"v1\.5\.0 \*\(baseline\)\* \| \d+ / (\d+)", "ab_total"), "README.md",
"A/B baseline pass",
AB_BASELINE_LABEL + r" \*\(baseline\)\* \| (\d+) / \d+",
"ab_old_pass",
),
(
"README.md",
"A/B suite denominator",
AB_BASELINE_LABEL + r" \*\(baseline\)\* \| \d+ / (\d+)",
"ab_total",
),
("benchmarks/README.md", "A/B suite size", r"(\d+) checks, ezdxf backend", "ab_total"), ("benchmarks/README.md", "A/B suite size", r"(\d+) checks, ezdxf backend", "ab_total"),
( (
"benchmarks/README.md", "benchmarks/README.md",
"A/B baseline pass", "A/B baseline pass",
r"\*\*v1\.5\.0\*\* \(baseline\)\s*\| (\d+) / \d+", r"\*\*" + AB_BASELINE_LABEL + r"\*\* \(baseline\)\s*\| (\d+) / \d+",
"ab_old_pass", "ab_old_pass",
), ),
( (
"benchmarks/README.md", "benchmarks/README.md",
"A/B suite denominator", "A/B suite denominator",
r"\*\*v1\.5\.0\*\* \(baseline\)\s*\| \d+ / (\d+)", r"\*\*" + AB_BASELINE_LABEL + r"\*\* \(baseline\)\s*\| \d+ / (\d+)",
"ab_total", "ab_total",
), ),
( (
@@ -136,25 +153,26 @@ CHECKS: list[tuple[str, str, str, str]] = [
( (
"README.md", "README.md",
"A/B baseline pass rate", "A/B baseline pass rate",
r"v1\.5\.0 \*\(baseline\)\* \| \d+ / \d+ \| ([\d.]+) %", AB_BASELINE_LABEL + r" \*\(baseline\)\* \| \d+ / \d+ \| ([\d.]+) %",
"ab_old_pct", "ab_old_pct",
), ),
( (
"README.md", "README.md",
"A/B fixed count", "A/B fixed count",
r"v1\.5\.1\*\* \*\(this release\)\* \| \*\*\d+ / \d+\*\* \| \*\*\d+ %\*\* \| (\d+)", AB_CURRENT_LABEL
+ r"\*\* \*\(this branch\)\* \| \*\*\d+ / \d+\*\* \| \*\*\d+ %\*\* \| (\d+)",
"ab_fixed", "ab_fixed",
), ),
( (
"benchmarks/README.md", "benchmarks/README.md",
"A/B baseline pass rate", "A/B baseline pass rate",
r"\*\*v1\.5\.0\*\* \(baseline\)\s*\| \d+ / \d+ \| ([\d.]+) %", r"\*\*" + AB_BASELINE_LABEL + r"\*\* \(baseline\)\s*\| \d+ / \d+ \| ([\d.]+) %",
"ab_old_pct", "ab_old_pct",
), ),
( (
"benchmarks/README.md", "benchmarks/README.md",
"A/B fixed count", "A/B fixed count",
r"\*\*v1\.5\.1\*\* \(this release\) \| \d+ / \d+ \| \d+ % \| (\d+)", r"\*\*" + AB_CURRENT_LABEL + r"\*\* \(this branch\) \| \d+ / \d+ \| \d+ % \| (\d+)",
"ab_fixed", "ab_fixed",
), ),
("CLAUDE.md", "A/B suite size", r"correctness_suite\.py` \((\d+) checks\)", "ab_total"), ("CLAUDE.md", "A/B suite size", r"correctness_suite\.py` \((\d+) checks\)", "ab_total"),
@@ -169,8 +187,8 @@ def run(verbose: bool = False) -> int:
print( print(
f"DRIFT correctness_suite.py has {values['suite_checks']} checks but the published " f"DRIFT correctness_suite.py has {values['suite_checks']} checks but the published "
f"A/B report was run over {values['ab_total']} -- regenerate the report:\n" f"A/B report was run over {values['ab_total']} -- regenerate the report:\n"
f" python benchmarks/compare_versions.py v1.5.0 " f" python benchmarks/compare_versions.py {values['ab_baseline_ref']} "
f"--json benchmarks/results/published/ab-v1.5.0-vs-v1.5.1.json" f"--json {AB_REPORT}"
) )
return 1 return 1
+86
View File
@@ -0,0 +1,86 @@
"""Exercise the P&ID track once against the live AutoCAD on this machine.
Creates a NEW document (never touches the operator's open drawing), then runs
the whole track through the same code the tools call:
apply_layer_set("pid") -> run_spec(EXAMPLE_SPEC)
= block_define -> pid_symbol_insert -> ACADMCP_PID xdata
-> pid_line_draw -> pid_graph -> six pid_* critique focuses
-> entity_get_xdata on the pump
and prints the graph stats as JSON. Exit codes:
0 every line landed on a port (``dangling == 0``)
1 the sheet drew but the reader found a dangling end
2 no live CAD application reachable (pywin32 missing, or the ProgID
cannot be created) -- the COM paths then remain fake-tested only
The backend's ``connect()`` is lazy on purpose (the server must start before
AutoCAD is open), so reachability is read from ``system_status()`` rather than
from ``connect()``. Note that the backend's ``Dispatch`` fallback *launches*
the ProgID when nothing is running; a cold AutoCAD start can take longer than
``COM_CALL_TIMEOUT`` (60 s), so open AutoCAD first when the first call times
out.
AUTOCAD_MCP_BACKEND=com uv run --frozen python scripts/smoke_pid_com.py
uv run --frozen python scripts/smoke_pid_com.py --keep # leave the document open
"""
from __future__ import annotations
import asyncio
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
XDATA_APP = "ACADMCP_PID"
async def main(keep_document: bool = False) -> int:
try:
from backends.com_backend import ComBackend
except ImportError as exc:
print(f"pywin32 not available: {exc}")
return 2
backend = ComBackend()
try:
await backend.connect()
except Exception as exc:
print(f"no live AutoCAD reachable: {exc}")
return 2
try:
status = await backend.system_status()
if not status.get("connected"):
print(f"no live AutoCAD reachable: {status.get('error', status)}")
return 2
from engineering.layers import apply_layer_set
from engineering.pid.spec import EXAMPLE_SPEC, run_spec
await backend.drawing_new()
await apply_layer_set(backend, "pid")
result = await run_spec(backend, EXAMPLE_SPEC)
got = await backend.entity_get_xdata(result["handles"]["P-101"], XDATA_APP)
report = {
"autocad": status.get("autocad_version") or status.get("cad_progid"),
"active_document": (await backend.system_status()).get("active_document"),
"handles": result["handles"],
"graph_stats": result["graph"]["stats"],
"crossings_total": result["crossings_total"],
"critique_issues": len(result["critique"]),
"critique": result["critique"],
"xdata_chunks": len(got["xdata"].get(XDATA_APP, [])),
}
print(json.dumps(report, indent=2))
if not keep_document:
await backend.drawing_close(save=False)
return 0 if result["graph"]["stats"]["dangling"] == 0 else 1
finally:
await backend.disconnect()
if __name__ == "__main__":
raise SystemExit(asyncio.run(main(keep_document="--keep" in sys.argv[1:])))