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:
co-authored by
Claude Opus 5
parent
8d9c444b76
commit
2be5e4180d
+169
@@ -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/),
|
||||
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
|
||||
|
||||
A patch for three defects found *after* 1.5.0 went to PyPI, two of them by the
|
||||
|
||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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`)
|
||||
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
|
||||
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`
|
||||
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)
|
||||
9. View & Screenshot (4 tools — includes `view_zoom_and_screenshot`): `view_*`
|
||||
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
|
||||
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).
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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
|
||||
|
||||
@@ -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.
|
||||
- **`_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.
|
||||
- **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.
|
||||
|
||||
### Premium Drawing Rules
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
> **v1.5 release snapshot:** 166 tools · 8 resources · 5 prompt templates · 1369 collected tests.
|
||||
> 154 is the **registered** count; a default install advertises 149 over `tools/list`,
|
||||
> **v1.5 release snapshot:** 166 tools · 8 resources · 5 prompt templates · 2423 collected tests.
|
||||
> 166 is the **registered** count; a default install advertises 161 over `tools/list`,
|
||||
> because `ENABLE_3D` is unset. `system_about` is the runtime authority.
|
||||
|
||||
## 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 |
|
||||
|---|---:|---:|
|
||||
| `TOOL_PROFILE=full` (default) | 149 | 40,305 tokens |
|
||||
| `TOOL_PROFILE=lean` | 47 | 12,131 tokens |
|
||||
| `TOOL_PROFILE=full` (default) | 161 | 44,930 tokens |
|
||||
| `TOOL_PACKS=core` (full profile) | 152 | 41,420 tokens |
|
||||
| `TOOL_PROFILE=lean` | 50 | 14,007 tokens |
|
||||
| `DISCOVERY_MODE=search` | 2 | **356 tokens** |
|
||||
|
||||
> [!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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -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) |
|
||||
| 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.
|
||||
|
||||
@@ -147,22 +157,23 @@ Self-measurement, produced by scripts in [`benchmarks/`](https://github.com/U-C4
|
||||
|
||||
| Version | Checks passing | Pass rate | Fixed | Regressed |
|
||||
|---|---:|---:|---:|---:|
|
||||
| v1.5.0 *(baseline)* | 24 / 26 | 92.3 % | — | — |
|
||||
| **v1.5.1** *(this release)* | **26 / 26** | **100 %** | 2 | **0** |
|
||||
| v1.5.1 *(baseline)* | 26 / 29 | 89.7 % | — | — |
|
||||
| **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 |
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `selection_filter` | window 1, crossing 2, bounding box 3, polygon 1 |
|
||||
| `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
|
||||
|
||||
@@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
Nothing loads a `.env` file — export these, or set them in your MCP client's `env` block.
|
||||
|
||||
+23
-7
@@ -214,29 +214,45 @@ python benchmarks/compare_versions.py v1.0.0 # vs a tag/ref
|
||||
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:
|
||||
[`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).
|
||||
|
||||
| Version | Checks passing | Pass rate | Fixed | Regressed |
|
||||
|---------|----------------|-----------|-------|-----------|
|
||||
| **v1.5.0** (baseline) | 24 / 26 | 92.3 % | — | — |
|
||||
| **v1.5.1** (this release) | 26 / 26 | 100 % | 2 | **0** |
|
||||
| v1.5.0 (baseline) | 24 / 26 | 92.3 % | — | — |
|
||||
| v1.5.1 (that release) | 26 / 26 | 100 % | 2 | **0** |
|
||||
|
||||
v1.5.0 fails `diameter_dim_measures_the_diameter` and
|
||||
`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.
|
||||
|
||||
### 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).
|
||||
|
||||
| Version | Checks passing | Pass rate | Fixed | Regressed |
|
||||
|---------|----------------|-----------|-------|-----------|
|
||||
| **v1.4.0** (baseline) | 21 / 26 | 80.8 % | — | — |
|
||||
| **v1.5.1** (this release) | 26 / 26 | 100 % | 5 | **0** |
|
||||
| v1.4.0 (baseline) | 21 / 26 | 80.8 % | — | — |
|
||||
| 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
|
||||
> 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
|
||||
|
||||
**Date:** 2026-09-15 · **Status:** approved; implementation plan at
|
||||
`docs/superpowers/plans/2026-09-15-v1.6-pid-implementation.md` · **Parent:** `2026-09-15-v1.6-roadmap-design.md` (track A) ·
|
||||
**Date:** 2026-09-15 · **Status:** implemented 2026-09-16 (21 tasks of
|
||||
`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
|
||||
|
||||
## 1. Summary
|
||||
@@ -104,25 +104,28 @@ get the whole track for the price of the two new contracts.
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Port:
|
||||
name: str # "in", "out", "suction", "discharge", "N1", "signal"
|
||||
name: str # "in", "out", "suction", "discharge", "N1", "signal"
|
||||
x: float
|
||||
y: float
|
||||
direction_deg: float | None # outward direction a line leaves along; None = radial
|
||||
direction_deg: float | None # outward direction a line leaves along; None = radial
|
||||
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)
|
||||
class SymbolSpec:
|
||||
name: str # block name, see 4.5
|
||||
family: Literal["valve", "rotating", "vessel", "heat", "misc", "instrument", "connector", "marker"]
|
||||
primitives: tuple[dict, ...] # block_define entity specs (section 5.1)
|
||||
attdefs: tuple[dict, ...] # block_define attdef specs
|
||||
name: str # block name, see 4.5
|
||||
family: Literal[
|
||||
"valve", "rotating", "vessel", "heat", "misc", "instrument", "connector", "marker"
|
||||
]
|
||||
primitives: tuple[dict, ...] # block_define entity specs (section 5.1)
|
||||
attdefs: tuple[dict, ...] # block_define attdef specs
|
||||
ports: tuple[Port, ...]
|
||||
bbox: tuple[float, float, float, float] # xmin, ymin, xmax, ymax — declared, tested
|
||||
bbox: tuple[float, float, float, float] # xmin, ymin, xmax, ymax — declared, tested
|
||||
layer_class: Literal["PROCESS-VALVES", "PROCESS-EQUIPMENT", "INSTRUMENT-SYMBOL", "line"]
|
||||
source: str
|
||||
parametric: bool = False
|
||||
params: dict = field(default_factory=dict) # the parameters a parametric spec was built from
|
||||
params: dict = field(default_factory=dict) # the parameters a parametric spec was built from
|
||||
```
|
||||
|
||||
`CATALOG_VERSION = "1"` is written into every INSERT's XDATA. A change to any
|
||||
@@ -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:
|
||||
"""→ {"handle", "xdata": {app: [value, ...]}, "backend"}; unknown app → {} not an error"""
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def entity_set_xdata(self, handle: str, app_name: str, values: list) -> dict:
|
||||
"""→ {"ok": True, "handle", "app_name", "value_count", "bytes", "backend"}"""
|
||||
|
||||
@@ -27,6 +27,12 @@ from pathlib import Path
|
||||
|
||||
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:
|
||||
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."""
|
||||
sys.path.insert(0, str(ROOT))
|
||||
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 = {
|
||||
w["workload_id"]: w["wall_ms"]
|
||||
for w in _json("benchmarks/results/published/perf-ezdxf.json")["workloads"]
|
||||
@@ -81,6 +87,7 @@ def derived() -> dict[str, object]:
|
||||
"resources": inventory["resources"],
|
||||
"prompts": inventory["prompts"],
|
||||
"groups": inventory["groups"],
|
||||
"ab_baseline_ref": ab["baseline_ref"],
|
||||
"ab_total": ab["total"],
|
||||
"ab_old_pass": ab["old_pass"],
|
||||
"ab_old_pct": ab["old_pct"],
|
||||
@@ -112,19 +119,29 @@ CHECKS: list[tuple[str, str, str, str]] = [
|
||||
"prompts",
|
||||
),
|
||||
("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 baseline pass",
|
||||
r"\*\*v1\.5\.0\*\* \(baseline\)\s*\| (\d+) / \d+",
|
||||
r"\*\*" + AB_BASELINE_LABEL + r"\*\* \(baseline\)\s*\| (\d+) / \d+",
|
||||
"ab_old_pass",
|
||||
),
|
||||
(
|
||||
"benchmarks/README.md",
|
||||
"A/B suite denominator",
|
||||
r"\*\*v1\.5\.0\*\* \(baseline\)\s*\| \d+ / (\d+)",
|
||||
r"\*\*" + AB_BASELINE_LABEL + r"\*\* \(baseline\)\s*\| \d+ / (\d+)",
|
||||
"ab_total",
|
||||
),
|
||||
(
|
||||
@@ -136,25 +153,26 @@ CHECKS: list[tuple[str, str, str, str]] = [
|
||||
(
|
||||
"README.md",
|
||||
"A/B baseline pass rate",
|
||||
r"v1\.5\.0 \*\(baseline\)\* \| \d+ / \d+ \| ([\d.]+) %",
|
||||
AB_BASELINE_LABEL + r" \*\(baseline\)\* \| \d+ / \d+ \| ([\d.]+) %",
|
||||
"ab_old_pct",
|
||||
),
|
||||
(
|
||||
"README.md",
|
||||
"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",
|
||||
),
|
||||
(
|
||||
"benchmarks/README.md",
|
||||
"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",
|
||||
),
|
||||
(
|
||||
"benchmarks/README.md",
|
||||
"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",
|
||||
),
|
||||
("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(
|
||||
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" python benchmarks/compare_versions.py v1.5.0 "
|
||||
f"--json benchmarks/results/published/ab-v1.5.0-vs-v1.5.1.json"
|
||||
f" python benchmarks/compare_versions.py {values['ab_baseline_ref']} "
|
||||
f"--json {AB_REPORT}"
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
@@ -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:])))
|
||||
Reference in New Issue
Block a user