docs: v1.6 roadmap decomposition and the Track A (P&ID) design spec
Research snapshot of the AutoCAD MCP field (20 READMEs, 2026-09-14), the seven 1.6 tracks with measurable acceptance criteria, their order (A -> E -> B -> G -> F -> H -> D), and the tool-pack decision. Track A: symbol catalogue authored in code (ISO 10628-2 + ISA-5.1), two generic contracts (block_define, entity_get/set_xdata), nine pid_* tools, routing, the ISA-5.1 tag grammar, the pid_graph reader, six critique focuses, TOOL_PACKS, and the test/evidence plan. 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
abc2a82e71
commit
3430573de1
@@ -0,0 +1,814 @@
|
||||
# v1.6 Track A — P&ID expert and reader
|
||||
|
||||
**Date:** 2026-09-15 · **Status:** design approved in conversation, awaiting
|
||||
written review · **Parent:** `2026-09-15-v1.6-roadmap-design.md` (track A) ·
|
||||
**Baseline:** v1.5.1
|
||||
|
||||
## 1. Summary
|
||||
|
||||
v1.5.1 supports P&IDs with a prompt template that tells the model to hand-draw a
|
||||
pump as a circle plus a triangle — the "no coordinate guessing" rule, broken by
|
||||
our own prompt. Three rival servers ship a `pid` tool, all bound to the
|
||||
commercial CAD Tools Online symbol library.
|
||||
|
||||
This track makes the server a P&ID **expert** (a symbol catalogue authored in
|
||||
code, real blocks with tags and ports, orthogonal line drawing with ISA-5.1 line
|
||||
classes and line numbers, an ISA-5.1 tag grammar) and a P&ID **reader**
|
||||
(`pid_graph` turns any P&ID — ours or foreign — into a connectivity graph, and
|
||||
derives the instrument index, line list and equipment list from it). Six
|
||||
critique focuses make P&ID correctness part of `drawing_finalize`'s score.
|
||||
|
||||
Two generic contracts are added because the catalogue needs them and nothing in
|
||||
v1.5.1 provides them: `block_define` (a block definition with ATTDEFs from typed
|
||||
primitive specs) and `entity_get_xdata` / `entity_set_xdata`.
|
||||
|
||||
Tool budget: **+12** (3 generic, 9 `pid_*`) → 166 registered.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
**In:** everything in sections 4–13. **Out of 1.6** (recorded so nobody
|
||||
re-derives it): DEXPI/Proteus export (pyDEXPI is AGPL; writing Proteus XML from
|
||||
scratch is its own project), ISA-5.2 binary-logic symbols, obstacle-avoiding
|
||||
routing, off-page links across *files* (same-file links are in), electrical
|
||||
IEC 60617 symbols (1.7, on this infrastructure), reading DWG headlessly (DWG is
|
||||
read by the live backend; headless reads DXF).
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
```
|
||||
engineering/pid/
|
||||
__init__.py public surface re-exported by engineering/__init__.py
|
||||
symbols.py SymbolSpec catalogue (pure data + generators), ports, naming
|
||||
geometry.py primitive-spec helpers shared by symbols (arcs, polygons, text)
|
||||
tags.py ISA-5.1 grammar, letter tables, equipment-prefix map
|
||||
lines.py line classes, routing candidates, labels, markers, arrows
|
||||
xdata.py ACADMCP_PID payload encode/decode (chunking)
|
||||
graph.py PidGraph builder: nodes/edges/junctions/dangling, foreign heuristics
|
||||
deliverables.py instrument index, line list, equipment list rows (+CSV)
|
||||
spec.py pid_from_spec schema validation and execution
|
||||
critique.py the six pid_* focuses (registered into engineering.critique)
|
||||
|
||||
backends/contracts/blocks.py + block_define
|
||||
backends/contracts/entity_query.py + entity_get_xdata / entity_set_xdata
|
||||
backends/ezdxf_backend.py, backends/com_backend.py implementations
|
||||
server.py SECTION 17: P&ID (9 tools) + 3 generic tools in
|
||||
their existing sections; TOOL_PACKS; 2 resources
|
||||
discovery/aliases.py 12 alias records
|
||||
```
|
||||
|
||||
Data flow for drawing: `pid_symbol_insert` → catalogue → `block_define` (once
|
||||
per block name per document) → `block_insert(attributes=…)` → `entity_set_xdata`
|
||||
→ returns handle + WCS ports. `pid_line_draw` → resolve ports → routing →
|
||||
`entity_create_polyline` on the class layer → label `entity_create_text` →
|
||||
marker/arrow `block_insert` → `entity_set_xdata`.
|
||||
|
||||
Data flow for reading: `pid_graph` → `entity_list` (paged, one pass) +
|
||||
`block_get_attributes` per INSERT + `entity_get_xdata` per candidate →
|
||||
classification → spatial index → endpoint resolution → graph → deliverables and
|
||||
critique consume the graph, never the drawing directly.
|
||||
|
||||
Every geometry write goes through existing backend primitives, so both engines
|
||||
get the whole track for the price of the two new contracts.
|
||||
|
||||
## 4. Symbol catalogue (`engineering/pid/symbols.py`)
|
||||
|
||||
### 4.1 Standards
|
||||
|
||||
- Instrumentation (bubbles, signal line classes, identification letters):
|
||||
**ISA-5.1-2009**.
|
||||
- Equipment, valves, fittings: **ISO 10628-2:2012** shapes. Where ISO 10628-2
|
||||
has no symbol (e.g. spectacle blind), the PIP PIC001 shape is used and the
|
||||
record's `source` says so.
|
||||
- Every `SymbolSpec` carries `source: str` naming the standard and figure/table
|
||||
it was authored from. A test asserts the field is non-empty and matches
|
||||
`^(ISO 10628-2|ISA-5\.1|PIP PIC001)`.
|
||||
|
||||
### 4.2 Units and conventions
|
||||
|
||||
- Symbol-local coordinates in **millimetres at A3/A1 paper scale**; the
|
||||
insertion `scale` parameter handles other sheets. Reference sizes:
|
||||
instrument bubble Ø10; valve body 8 × 4 (two triangles meeting at the
|
||||
centre); pump circle Ø8; shell-and-tube exchanger 24 × 8; default vertical
|
||||
vessel 20 × 40; grid 5. Text: equipment/valve `TAG` height 2.5, bubble
|
||||
`FUNC`/`LOOP` height 2.0 (ISO 3098 sizes).
|
||||
- Origin `(0, 0)` = insertion point = centre of the body's bounding box.
|
||||
Rotation 0 = flow left→right (process `in` port on −X, `out` on +X).
|
||||
- All primitives inside the block are on layer `"0"` with colour/linetype/
|
||||
lineweight **ByBlock**, so the INSERT's layer (section 4.6) supplies the ISO
|
||||
128 lineweight. Filled shapes (check-valve flap, flow arrow) are closed
|
||||
polylines plus a SOLID-pattern hatch spec; `block_define` supports
|
||||
`{"type": "solid", "points": [...]}` for exactly this.
|
||||
|
||||
### 4.3 Record structure
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Port:
|
||||
name: str # "in", "out", "suction", "discharge", "N1", "signal"
|
||||
x: float
|
||||
y: float
|
||||
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
|
||||
|
||||
@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
|
||||
ports: tuple[Port, ...]
|
||||
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
|
||||
```
|
||||
|
||||
`CATALOG_VERSION = "1"` is written into every INSERT's XDATA. A change to any
|
||||
shipped symbol's geometry or ports bumps it; the graph builder reads geometry
|
||||
from the drawing, not the catalogue, so old drawings keep working.
|
||||
|
||||
### 4.4 Catalogue v1
|
||||
|
||||
Base symbols (41) — every one is a test case:
|
||||
|
||||
| Family | Symbols | Ports |
|
||||
|---|---|---|
|
||||
| valve (11 bodies) | `gate`, `globe`, `ball`, `butterfly`, `check`, `needle`, `plug`, `diaphragm`, `three_way`, `angle`, `relief` | `in`(−X), `out`(+X); `three_way` adds `branch`(−Y); `angle`: `in`(−X), `out`(−Y); `relief`: `in`(−Y), `out`(+X) |
|
||||
| valve actuators (6, composed onto any body except `check`/`relief`) | `none`, `diaphragm`, `piston`, `motor`, `solenoid`, `hand` | adds `signal`(+Y) for every actuator except `none`/`hand`; fail position attribute `FAIL` ∈ {"", "FO", "FC", "FL"} drawn as text beside the actuator |
|
||||
| rotating (6) | `centrifugal_pump`, `pd_pump`, `compressor`, `fan`, `agitator`, `ejector` | pumps: `suction`(−X), `discharge`(+Y); compressor/fan: `in`(−X), `out`(+X); agitator: `shaft`(−Y, process, mounts on a vessel); ejector: `motive`(−X), `suction`(−Y), `out`(+X) |
|
||||
| vessel (6, parametric) | `vertical_vessel`, `horizontal_vessel`, `tank` (`roof` ∈ {flat, cone}), `column` (`trays` int ≥ 0), `reactor` (`jacketed` bool), `hopper` | nozzles from `params["nozzles"] = [{"name","side","fraction"}]`, side ∈ {top, bottom, left, right}, fraction ∈ [0,1] along that side; defaults: vertical_vessel N1 top, N2 bottom, N3 left 0.7, N4 right 0.3 |
|
||||
| heat (4) | `shell_tube_hx`, `plate_hx`, `air_cooler`, `fired_heater` | shell: `shell_in`(+Y), `shell_out`(−Y), `tube_in`(−X), `tube_out`(+X); others: `in`/`out` per side |
|
||||
| misc (7) | `filter`, `strainer_y`, `reducer` (`concentric`/`eccentric`), `flange_pair`, `spectacle_blind`, `orifice_plate`, `drain_vent` | inline: `in`(−X), `out`(+X); `orifice_plate` adds `signal`(+Y); `drain_vent`: `in`(+Y) only |
|
||||
| instrument (1 generator → 20) | `type` ∈ {`discrete`, `dcs`, `computer`, `plc`} × `location` ∈ {`field`, `primary`, `auxiliary`, `primary_rear`, `auxiliary_rear`} | one radial `signal` port, radius 5 |
|
||||
| connector (3) | `offpage` (`direction` ∈ {in, out}), `tie_in`, `spec_break` | `offpage`: one `process` port on the open side; `tie_in`/`spec_break`: `in`/`out` |
|
||||
| marker (5) | `arrow_flow`, `mark_pneumatic` (//), `mark_capillary` (X), `mark_hydraulic` (L), `mark_data` (o) | none — markers are decoration on a line and never graph nodes |
|
||||
|
||||
Instrument bubble by ISA-5.1-2009 Table 5.4.1: `discrete` = circle;
|
||||
`dcs` = circle inside a square; `computer` = hexagon; `plc` = diamond inside a
|
||||
square. Location line through the middle: `field` none; `primary` one solid;
|
||||
`auxiliary` two solid; `primary_rear` one dashed; `auxiliary_rear` two dashed
|
||||
(dashes drawn as short line primitives, 1 mm on / 1 mm off, so no linetype
|
||||
dependency inside the block).
|
||||
|
||||
### 4.5 Block naming
|
||||
|
||||
`PID_<FAMILY>_<SYMBOL>[_<VARIANT>]`, upper case, e.g. `PID_VALVE_GATE`,
|
||||
`PID_VALVE_GATE_DIAPHRAGM`, `PID_ROTATING_CENTRIFUGAL_PUMP`,
|
||||
`PID_INST_DCS_PRIMARY`, `PID_CONNECTOR_OFFPAGE_OUT`, `PID_MARKER_ARROW_FLOW`.
|
||||
Parametric vessels: `PID_VESSEL_VERTICAL_<8 hex>` where the hex is the first 8
|
||||
characters of SHA-256 over the canonical JSON of `params` — identical
|
||||
parameters share one definition, different ones never collide in practice.
|
||||
|
||||
### 4.6 Layers
|
||||
|
||||
INSERTs go on `layer_class`: valves → `PROCESS-VALVES`, rotating/vessel/heat/
|
||||
misc/connector → `PROCESS-EQUIPMENT`, instruments → `INSTRUMENT-SYMBOL`,
|
||||
markers → the layer of the line they decorate. `PID_LAYERS` gains
|
||||
`("PROCESS-LINE-TEXT", 7, "Continuous", 0.25, "line numbers")` as its 16th
|
||||
entry. `pid_symbol_insert` and `pid_line_draw` create a missing target layer
|
||||
from the `PID_LAYERS` definition (idempotent, same code path as
|
||||
`drawing_apply_iso_layers`) and report it in `layers_created`; they do not
|
||||
apply the whole set.
|
||||
|
||||
### 4.7 ATTDEFs
|
||||
|
||||
| Family | Tags | Placement (symbol-local) |
|
||||
|---|---|---|
|
||||
| valve, rotating, vessel, heat, misc, connector | `TAG` (height 2.5, centred at `(0, ymax + 3.5)`); `DESC` (height 2.0, centred at `(0, ymax + 6.5)`, visible; `pid_symbol_insert` writes `""` when no description is given — an empty ATTRIB renders nothing, and per-insert invisibility is not something `block_insert(attributes=…)` exposes) | above the body |
|
||||
| valve with actuator | + `FAIL` (height 2.0, right of the actuator) | |
|
||||
| instrument | `FUNC` (height 2.0, middle-centre at `(0, +1.25)`), `LOOP` (height 2.0, middle-centre at `(0, −3.25)`) | the two halves of the bubble |
|
||||
| offpage | `TAG` inside the arrow box; `LINK` invisible | |
|
||||
|
||||
Attribute values are what `block_get_attributes` returns and what the reader
|
||||
uses first; XDATA (section 5.2) duplicates the machine-readable part.
|
||||
|
||||
## 5. Generic contracts
|
||||
|
||||
### 5.1 `block_define`
|
||||
|
||||
```python
|
||||
@abstractmethod
|
||||
async def block_define(
|
||||
self,
|
||||
name: str,
|
||||
entities: list[dict],
|
||||
attdefs: list[dict] | None = None,
|
||||
base_x: float = 0.0,
|
||||
base_y: float = 0.0,
|
||||
overwrite: bool = False,
|
||||
) -> dict:
|
||||
"""→ {"ok": True, "name", "entity_count", "attdef_count", "replaced": bool, "backend"}"""
|
||||
```
|
||||
|
||||
Entity specs (all coordinates block-local; `layer` optional, default `"0"`;
|
||||
`color`/`linetype`/`lineweight` optional, default ByBlock):
|
||||
|
||||
| `type` | Required keys |
|
||||
|---|---|
|
||||
| `line` | `x1, y1, x2, y2` |
|
||||
| `circle` | `cx, cy, r` (r > 0) |
|
||||
| `arc` | `cx, cy, r, start_deg, end_deg` (CCW, r > 0) |
|
||||
| `polyline` | `points: [[x, y], ...]` (≥ 2), `closed: bool`, optional `bulges: [float, ...]` same length as `points` |
|
||||
| `text` | `text, x, y, height` (> 0), optional `rotation_deg` (0), `align` ∈ {`left`, `center`, `right`, `middle_center`} (`left`) |
|
||||
| `solid` | `points` (3 or 4) — written as a closed polyline plus a SOLID hatch |
|
||||
|
||||
ATTDEF specs: `tag` (`^[A-Z][A-Z0-9_]{0,30}$`, unique within the block),
|
||||
`prompt` (default = tag), `default` (default `""`), `x, y, height`, optional
|
||||
`rotation_deg`, `align` (as text), `invisible: bool` (False).
|
||||
|
||||
Rules, all tested:
|
||||
|
||||
- The whole request is validated before anything is written (the
|
||||
`hatch_add_boundary` pattern). An invalid entry raises `TypeError` naming the
|
||||
index and the key; no block is created or modified.
|
||||
- `entities` empty → `ValueError` ("nothing to define") — a definition with
|
||||
only attributes is refused too, because it draws nothing.
|
||||
- Name: `sanitize_symbol_name(name, kind="block")` (existing helper);
|
||||
anonymous names (`*U…`) refused.
|
||||
- Name exists and `overwrite=False` → `ValueError` naming the block and the
|
||||
flag. `overwrite=True` **clears the existing definition's entities and
|
||||
re-adds** rather than deleting the block, so existing INSERTs keep pointing
|
||||
at the name and now show the new geometry; `replaced: true` says so. ezdxf:
|
||||
`blk.delete_all_entities()`; COM: iterate and `Delete()` each entity in the
|
||||
Block object. (Deleting a block that has references is refused by both
|
||||
engines; replacing contents is the semantics that works on both.)
|
||||
- ezdxf: `doc.blocks.new(name, base_point=…)`, `add_line/add_circle/add_arc/
|
||||
add_lwpolyline/add_text/add_attdef/add_hatch`. COM: `Blocks.Add(pt, name)`,
|
||||
then `AddLine/AddCircle/AddArc/AddLightWeightPolyline/AddText/AddAttribute/
|
||||
AddHatch` on the Block object; `AddAttribute(height, mode, prompt, pt, tag,
|
||||
value)` with `mode = acAttributeModeInvisible (1)` for invisible ATTDEFs,
|
||||
else `acAttributeModeNormal (0)`; text alignment through `Alignment` +
|
||||
`TextAlignmentPoint`.
|
||||
- Both engines: abstract (no `@capability`) — both can do it.
|
||||
|
||||
Registered as `block_define` in SECTION 7 (Blocks), tags `{"block", "create"}`,
|
||||
cost `mutate`, aliases `acad=("BLOCK", "BEDIT", "ATTDEF")`.
|
||||
|
||||
### 5.2 `entity_get_xdata` / `entity_set_xdata`
|
||||
|
||||
```python
|
||||
@abstractmethod
|
||||
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"}"""
|
||||
```
|
||||
|
||||
- `app_name`: `^[A-Za-z_][A-Za-z0-9_]{0,30}$`; registered in the APPID table if
|
||||
missing (`doc.appids.add` / `RegisteredApplications.Add`).
|
||||
- Value typing by group code: `str` → 1000, `int` → 1071, `float` → 1040,
|
||||
`[x, y]` or `[x, y, z]` → 1010; anything else → `TypeError`. Strings longer
|
||||
than 255 characters → `ValueError` (DXF limit; callers chunk). Total encoded
|
||||
size > 16 KB → `ValueError` (AutoCAD limit) before any write. `values` empty
|
||||
→ the app's XDATA is **removed** (documented, tested).
|
||||
- `set` replaces the app's XDATA wholesale; other apps' XDATA untouched.
|
||||
- ezdxf: `entity.set_xdata / get_xdata / discard_xdata`. COM: `SetXData(types,
|
||||
values)` / `GetXData(app, types, values)` with variant arrays.
|
||||
- Both engines: abstract.
|
||||
|
||||
Registered in SECTION 5 (Entity Query): `entity_get_xdata` (tags `{"entity",
|
||||
"query"}`, cost `read`, aliases `acad=("XDATA", "XDLIST")`), `entity_set_xdata`
|
||||
(tags `{"entity", "modify"}`, cost `safe`).
|
||||
|
||||
`engineering/pid/xdata.py` encodes the P&ID payload as
|
||||
`["json", chunk1, chunk2, ...]` (≤ 255 chars each) under app `ACADMCP_PID` and
|
||||
decodes by joining every 1000-string after the `"json"` marker. Payloads:
|
||||
|
||||
```json
|
||||
{"v": 1, "kind": "symbol", "catalog": "1", "name": "PID_VALVE_GATE_DIAPHRAGM",
|
||||
"family": "valve", "symbol": "gate", "actuator": "diaphragm",
|
||||
"params": {}, "ports": {"in": [-4, 0, 180], "out": [4, 0, 0], "signal": [0, 9, 90]}}
|
||||
{"v": 1, "kind": "line", "class": "process_major", "number": "100-P-1001-CS1-IH",
|
||||
"size": "100", "service": "P", "spec": "CS1", "insulation": "IH",
|
||||
"from": {"handle": "2F", "port": "discharge"}, "to": {"handle": "31", "port": "N1"}}
|
||||
{"v": 1, "kind": "marker", "line": "3A"}
|
||||
```
|
||||
|
||||
Ports in XDATA are symbol-local; the reader applies the INSERT transform. They
|
||||
are written for every symbol so a reader without the catalogue (another tool,
|
||||
a future version) still sees them.
|
||||
|
||||
## 6. Tools (SECTION 17: P&ID, 9 tools)
|
||||
|
||||
All in pack `pid` (section 12). Tags `{"pid", ...}`; `pid` is appended to
|
||||
`_GROUP_TAG_PRIORITY` ahead of `entity` so `system_about` files them under one
|
||||
`pid` group.
|
||||
|
||||
### 6.1 `pid_symbol_list(family: str | None = None) -> dict`
|
||||
|
||||
Catalogue listing: `{"catalog_version", "symbols": [{"symbol", "family",
|
||||
"variants", "ports": [...], "parametric", "params_schema", "source"}]}`.
|
||||
Cost `read`. Also exposed as resource `autocad://pid/symbols` (section 13).
|
||||
|
||||
### 6.2 `pid_symbol_insert(...)`
|
||||
|
||||
```python
|
||||
pid_symbol_insert(symbol: str, x: float, y: float, rotation: float = 0.0, scale: float = 1.0,
|
||||
tag: str | None = None, desc: str | None = None,
|
||||
actuator: str | None = None, fail: str | None = None,
|
||||
type: str | None = None, location: str | None = None, # instruments
|
||||
params: dict | None = None, # parametric
|
||||
link: str | None = None, # offpage
|
||||
layer: str | None = None)
|
||||
→ {"handle", "block_name", "defined": bool, "layers_created": [...],
|
||||
"ports": {"in": {"x","y","direction_deg","kind"}, ...}, # WCS
|
||||
"tag": str | None, "tag_warnings": [...], "backend"}
|
||||
```
|
||||
|
||||
- Instruments take `tag="FIC-101"` and split it through the grammar into
|
||||
`FUNC`/`LOOP` (area prefix and suffix kept in `LOOP` text as written, e.g.
|
||||
`101A`); or `func`/`loop` may be given in `params`. An invalid tag is
|
||||
**written and warned** (`tag_warnings`), never refused — critique catches it.
|
||||
- Unknown `symbol`/`actuator`/`type`/`location` → `ValueError` listing the
|
||||
valid values. `actuator` on `check`/`relief` → `ValueError`. `params` on a
|
||||
non-parametric symbol → `ValueError`; parametric symbols validate params
|
||||
(sizes > 0, nozzle sides/fractions in range, unique nozzle names).
|
||||
- `scale ≤ 0` → `ValueError`. `layer` overrides the family layer.
|
||||
- Defines the block on first use (`defined: true`), inserts with attributes,
|
||||
writes XDATA, returns WCS ports (rotation and scale applied).
|
||||
- Cost `mutate`. Aliases `acad=("INSERT",)`, synonyms "valve", "pump", "tank",
|
||||
"vessel", "instrument bubble", "p&id symbol", "control valve"...
|
||||
|
||||
### 6.3 `pid_line_draw(...)`
|
||||
|
||||
```python
|
||||
pid_line_draw(from_: dict, to: dict, line_class: str = "process_major",
|
||||
route: str | list = "auto", stub: float = 5.0,
|
||||
line_number: str | None = None, size: str | None = None, service: str | None = None,
|
||||
spec: str | None = None, insulation: str | None = None, number_format: str | None = None,
|
||||
label: bool = True, arrow: bool | None = None)
|
||||
→ {"handle", "vertices": [[x, y], ...], "line_class", "layer", "line_number",
|
||||
"from": {"handle","port","x","y"} | {"x","y"}, "to": ..., "port_reuse": [...],
|
||||
"crossings": int, "label_handle", "arrow_handle", "marker_handles": [...], "backend"}
|
||||
```
|
||||
|
||||
- `from_`/`to`: `{"handle": H, "port": name}` or `{"x": .., "y": ..}` (a free
|
||||
point, e.g. a junction on an existing line). A bare point gets no direction;
|
||||
routing treats it as radial.
|
||||
- Line classes (ISA-5.1 Table 5.3.1) → layer / linetype override / marker:
|
||||
|
||||
| class | layer | linetype | marker | arrow default |
|
||||
|---|---|---|---|---|
|
||||
| `process_major` | PROCESS-PIPING-MAIN | ByLayer (Continuous) | — | yes |
|
||||
| `process_minor` | PROCESS-PIPING-SECONDARY | ByLayer | — | yes |
|
||||
| `utility` | UTILITY-LINE | ByLayer (PHANTOM) | — | yes |
|
||||
| `pneumatic` | INSTRUMENT-LINE-SIGNAL | Continuous (override) | `mark_pneumatic` | no |
|
||||
| `electric` | INSTRUMENT-LINE-SIGNAL | ByLayer (DASHED) | — | no |
|
||||
| `hydraulic` | INSTRUMENT-LINE-SIGNAL | Continuous | `mark_hydraulic` | no |
|
||||
| `capillary` | INSTRUMENT-LINE-SIGNAL | Continuous | `mark_capillary` | no |
|
||||
| `data` | INSTRUMENT-LINE-SIGNAL | Continuous | `mark_data` | no |
|
||||
|
||||
Markers are blocks (section 4.4) placed at the midpoint of every segment
|
||||
≥ 15 mm, rotated with the segment; they are decoration, ignored by the
|
||||
reader. Rationale: SHX-shape complex linetypes are not portable across the
|
||||
two engines' renderers; blocks are.
|
||||
- Line number: given verbatim, or built from `number_format` (default
|
||||
`"{size}-{service}-{seq}-{spec}-{insulation}"`, empty fields and their
|
||||
separators dropped, `seq` auto-incremented per document from XDATA of
|
||||
existing lines). Label: TEXT height 2.5 on `PROCESS-LINE-TEXT`, 1.5 mm off
|
||||
the longest segment's midpoint on its left-hand side, rotated with the
|
||||
segment and never upside-down (angles in (90°, 270°] are flipped by 180°).
|
||||
- Arrow: `arrow_flow` block at the end point, rotated with the last segment;
|
||||
default per class, parameter overrides.
|
||||
- `crossings`: intersections between the new segments and existing P&ID lines
|
||||
(lines on P&ID line layers or carrying line XDATA), excluding touches at a
|
||||
shared endpoint. Reported, never refused.
|
||||
- `port_reuse`: ports that already had a line attached (branching is legal;
|
||||
the fact is reported).
|
||||
- Refusals (`ValueError`): same handle+port at both ends; unknown port (the
|
||||
message lists the symbol's ports); a marker/arrow block as an endpoint; any
|
||||
segment shorter than 0.01 mm; `route` waypoints that repeat a point; `stub`
|
||||
< 0.
|
||||
- One LWPOLYLINE per call. XDATA as in 5.2. Cost `mutate`. Aliases
|
||||
`acad=("PLINE",)` plus synonyms "process line", "pipe run", "signal line",
|
||||
"connect equipment", "instrument line".
|
||||
|
||||
### 6.4 `pid_tag_parse(tag: str, kind: str = "auto", equipment_prefixes: dict | None = None) -> dict`
|
||||
|
||||
Section 8. Cost `read`. Also used internally by insert and critique.
|
||||
|
||||
### 6.5 `pid_graph(...)`
|
||||
|
||||
```python
|
||||
pid_graph(tolerance: float = 0.5, label_search: float = 15.0,
|
||||
scope: str = "current_space", # or "all"
|
||||
include_foreign: bool = True, include_geometry: bool = False)
|
||||
→ {"nodes": [...], "edges": [...], "junctions": [...], "dangling": [...],
|
||||
"unclassified": [...], "offpage_links": [...],
|
||||
"stats": {"nodes_by_kind": {}, "edges_by_class": {}, "dangling": n,
|
||||
"unclassified": n, "confidence_min": float, "entities_scanned": n},
|
||||
"tolerance", "scope", "backend"}
|
||||
```
|
||||
|
||||
Section 9. Cost `read`. Aliases synonyms "p&id connectivity", "instrument
|
||||
index", "what is connected to", "trace the line".
|
||||
|
||||
### 6.6 `pid_instrument_index / pid_line_list / pid_equipment_list(csv_path: str | None = None, **graph_kwargs)`
|
||||
|
||||
Section 9.5. Each returns `{"rows": [...], "count", "csv_path": str | None,
|
||||
"graph_stats": {...}}`. `csv_path` goes through `validate_path(path,
|
||||
allow_write=True)`; the CSV header is the row keys in the documented order.
|
||||
Cost `read` — the drawing is never mutated; the optional CSV write is named in
|
||||
the tool summary so a client gating on cost is not surprised by a file.
|
||||
|
||||
### 6.7 `pid_from_spec(spec: dict, dry_run: bool = False) -> dict`
|
||||
|
||||
Section 10. Cost `mutate`.
|
||||
|
||||
## 7. Routing (`engineering/pid/lines.py`)
|
||||
|
||||
Inputs: start point `S`, start direction `dS` (unit axis vector or `None`),
|
||||
end point `E`, end direction `dE`, `stub`. Radial ports (bubbles, free points)
|
||||
take their direction from the vector towards the other end, snapped to the
|
||||
nearest axis, and their start point on the circle at `centre + r·dir`.
|
||||
|
||||
`route="auto"` enumerates candidates and picks the first that is valid, in this
|
||||
order; ties inside a candidate class are broken by total length:
|
||||
|
||||
1. **straight** — `S→E` when both lie on one axis line and each end's direction
|
||||
points towards the other (or is `None`).
|
||||
2. **L** — one bend at `(E.x, S.y)` or `(S.x, E.y)`; valid when the first
|
||||
segment leaves `S` along `dS` and the last arrives at `E` against `dE`.
|
||||
3. **Z** — two bends through the midline between the stub points, along
|
||||
`dS`'s axis; then along `dE`'s axis.
|
||||
4. **U** — `S→S₁→(corner)→E₁→E` with both stubs honoured when the ends face
|
||||
each other or away (three bends).
|
||||
|
||||
Validity for every candidate: the first segment is colinear with `dS` and
|
||||
≥ `stub` long, the last is colinear with `−dE` and ≥ `stub` long, no segment is
|
||||
shorter than `stub` except the whole route when `straight`, and consecutive
|
||||
segments are perpendicular. If no candidate is valid (e.g. `stub` larger than
|
||||
the gap), the tool raises `ValueError` naming the gap and the stub, so the
|
||||
caller can shorten the stub or pass waypoints. `route="direct"` draws `S→E`.
|
||||
`route=[...]` draws `S→w₁→…→E` verbatim after the refusal checks.
|
||||
|
||||
Everything here is pure geometry in `lines.py`, tested without a backend.
|
||||
|
||||
## 8. ISA-5.1 tag grammar (`engineering/pid/tags.py`)
|
||||
|
||||
Format: `^(?:(?P<area>\d{1,4})-)?(?P<letters>[A-Z]{1,5})[- ]?(?P<loop>\d{1,5})(?:[- ]?(?P<suffix>[A-Z]))?$`
|
||||
after upper-casing and trimming. Letters are then parsed with the grammar:
|
||||
|
||||
```
|
||||
tag := first modifier? readout* output* trailing?
|
||||
first := A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z
|
||||
modifier := D | F | K | Q | S # differential, ratio, rate of change, integrate, safety
|
||||
readout := A | B | E | G | I | L | N | O | P | R | U | W | X
|
||||
output := C | K | S | T | U | V | X | Y | Z
|
||||
trailing := HH | LL | H | L | M | O | C # high-high, low-low, high, low, middle, open, closed
|
||||
```
|
||||
|
||||
Disambiguation rules (fixed, tested):
|
||||
|
||||
- `S` in second position is the **safety** modifier only when followed by `V`
|
||||
or `E` (`PSV`, `TSE`); otherwise it is the **switch** output (`PSL`, `LSHH`).
|
||||
- `D` in second position is always **differential** (`PDT`, `PDIC`).
|
||||
- `F` in second position is **ratio** only when the first letter is also `F`
|
||||
(`FFC`); otherwise it is parsed as a readout error.
|
||||
- `K` in second position is **rate of change** only when followed by an output
|
||||
letter; `K` elsewhere is the control-station output (`HIK`).
|
||||
- `Q` in second position is **integrate** (`FQI`, `FQ`).
|
||||
- `H`, `L` and `M` are **trailing** modifiers (high/low/middle) when they follow
|
||||
`A` (alarm), `S` (switch) or another trailing letter — `LAL` is *Level Alarm
|
||||
Low*, `PSL` is *Pressure Switch Low*. `L` anywhere else is the readout
|
||||
*light* (`PL`).
|
||||
- `O` and `C` are **trailing** (open/closed) only as the last letter directly
|
||||
after `S` (`ZSO`, `ZSC`); otherwise `C` is the *control* output (`TIC`) and
|
||||
`O` the readout *orifice/restriction* (`FO`).
|
||||
- A first letter in `{C, D, G, M, N, O}` is *user's choice* and a readout/output
|
||||
in `{B, N, X}` is *unclassified*: the parse succeeds with
|
||||
`user_defined: true` and no description word for that letter.
|
||||
- Anything the grammar rejects → `valid: false` with `errors` naming the first
|
||||
offending letter and position; the tag is still echoed back.
|
||||
|
||||
Output:
|
||||
|
||||
```json
|
||||
{"tag": "10-FIC-101A", "valid": true, "kind": "instrument", "area": "10", "letters": "FIC",
|
||||
"first": {"letter": "F", "meaning": "Flow rate"}, "modifier": null,
|
||||
"readout": [{"letter": "I", "meaning": "Indicate"}], "output": [{"letter": "C", "meaning": "Control"}],
|
||||
"trailing": null, "loop": "101", "suffix": "A", "description": "Flow Indicating Controller",
|
||||
"user_defined": false, "errors": []}
|
||||
```
|
||||
|
||||
Equipment tags: `^(?:(?P<area>\d{1,4})-)?(?P<prefix>[A-Z]{1,3})-(?P<number>\d{1,5})(?P<suffix>[A-Z]{0,2})$`
|
||||
with the default prefix map `{P: pump, V: vessel, T: tank, E: exchanger,
|
||||
K: compressor, C: column, R: reactor, F: filter, D: drum, M: mixer, H: heater,
|
||||
S: separator, B: blower, X: package}` — ISO 10628 does not standardise these,
|
||||
so `equipment_prefixes` replaces the map per call and `kind="equipment"` forces
|
||||
the equipment parse. `kind="auto"` tries instrument first, then equipment.
|
||||
|
||||
Fixture: `tests/data/isa51_tags.json` with ≥ 40 valid tags and their expected
|
||||
descriptions and ≥ 10 invalid ones with the expected first error — the file a
|
||||
reader checks against ISA-5.1 Table 4.1.
|
||||
|
||||
## 9. Graph model and algorithm (`engineering/pid/graph.py`)
|
||||
|
||||
### 9.1 Model
|
||||
|
||||
```
|
||||
Node {id: handle, kind: instrument|equipment|valve|connector|unknown_block,
|
||||
block_name, family, symbol, tag, tag_parsed, tag_source: attribute|xdata|text|null,
|
||||
x, y, rotation_deg, scale, layer,
|
||||
ports: {name: {x, y, direction_deg, kind, inferred: bool, edges: [edge ids]}},
|
||||
source: catalog|xdata|heuristic|inferred, confidence: 1.0|0.95|0.6|0.3}
|
||||
Edge {id: handle, line_class, class_source: xdata|layer|null, line_number, number_source: xdata|label|null,
|
||||
size, service, spec, insulation, from: {node, port}|{junction}|null, to: ..., length,
|
||||
vertices (only with include_geometry), layer, source: xdata|layer|touch}
|
||||
Junction {id: "J<n>", x, y, edges: [...]} # a line endpoint on another line's vertex or segment
|
||||
Dangling {edge, end: "from"|"to", x, y, nearest: {node, port, distance}|null}
|
||||
Offpage {link, nodes: [...]} # connectors sharing a LINK value
|
||||
```
|
||||
|
||||
### 9.2 Algorithm
|
||||
|
||||
1. **Collect** with `entity_list` paged (`limit=1000`, `offset` loop) over the
|
||||
current space (or every layout when `scope="all"`, each entity tagged with
|
||||
its space).
|
||||
2. **Classify INSERTs**: block name in the catalogue (`PID_*`, markers and
|
||||
arrows excluded) → `catalog` 1.0; else XDATA `ACADMCP_PID` with
|
||||
`kind="symbol"` → `xdata` 0.95; else heuristic 0.6 when
|
||||
`include_foreign` and either an attribute tag in `{TAG, TAGNO, TAGNUMBER,
|
||||
EQUIP_TAG, EQUIPMENT_TAG, INST_TAG, INSTTAG}` exists or the block name
|
||||
contains one of `VALVE, VLV, PUMP, TANK, VESSEL, DRUM, INST, BUBBLE, HX,
|
||||
EXCH, FILTER, COMP` (kind from the keyword); else the INSERT is kept only if
|
||||
step 5 finds a line touching its bounding box → `unknown_block` 0.3,
|
||||
otherwise dropped (title blocks, logos, north arrows).
|
||||
3. **Ports**: catalogue/XDATA ports transformed by the INSERT's rotation and
|
||||
scale about its insertion point. Foreign/unknown blocks: ports are
|
||||
**inferred** in step 5 — every distinct line endpoint within `tolerance` of
|
||||
the block's bounding-box boundary becomes `p1..pn` with `inferred: true`.
|
||||
4. **Candidate edges**: LINE and LWPOLYLINE (and POLYLINE) entities that (a)
|
||||
carry line XDATA, or (b) lie on a P&ID line layer (`PROCESS-PIPING-MAIN`,
|
||||
`-SECONDARY`, `UTILITY-LINE`, `INSTRUMENT-LINE-SIGNAL`, `ELECTRICAL-LINE`),
|
||||
or (c) when `include_foreign`, touch a node's port or bounding box at an
|
||||
endpoint. `line_class` from XDATA, else from the layer:
|
||||
`PROCESS-PIPING-MAIN → process_major`, `PROCESS-PIPING-SECONDARY →
|
||||
process_minor`, `UTILITY-LINE → utility`, `ELECTRICAL-LINE → electric`,
|
||||
`INSTRUMENT-LINE-SIGNAL → electric` when the effective linetype is dashed,
|
||||
otherwise `signal_unknown` (`pneumatic` is never assumed from a plain line);
|
||||
any other layer → `null`.
|
||||
5. **Resolve endpoints** through a uniform grid (cell = 10·tolerance): a port
|
||||
within `tolerance` → `{node, port}`; else another edge's vertex within
|
||||
`tolerance` → junction; else a point within `tolerance` of another edge's
|
||||
segment interior → junction (the touched edge is not split); else
|
||||
`dangling` with the nearest port within `10·tolerance` as a hint.
|
||||
6. **Tags**: instrument `FUNC`+`LOOP` → `TAG` attribute → the TEXT/MTEXT whose
|
||||
insertion lies inside the bubble radius → null. Equipment/valve: `TAG`
|
||||
attribute → nearest TEXT within `label_search` above the bounding box →
|
||||
null. Line numbers: XDATA → nearest TEXT within `label_search` of the
|
||||
longest segment whose content matches `^\d+(?:"|mm)?-[A-Z]{1,4}-\d+` →
|
||||
null. Every source is reported.
|
||||
7. **Off-page links**: connectors with equal `LINK` values (same file) are
|
||||
joined in `offpage_links`; the reader does not invent an edge between them.
|
||||
8. **Stats** as in 6.5; `confidence_min` over nodes is the number a caller must
|
||||
read before trusting a foreign graph.
|
||||
|
||||
Complexity O((n + m)·k) with the grid; performance is measured in the perf
|
||||
lane on a 5 000-entity synthetic P&ID, not asserted in unit tests.
|
||||
|
||||
### 9.3 Foreign drawings
|
||||
|
||||
Headless: `drawing_open(dxf)` then `pid_graph`. Live: `drawing_open(dwg)`.
|
||||
CTO-library drawings classify through their `TAG`-style attributes and
|
||||
keyword block names at confidence 0.6 with inferred ports; a test fixture
|
||||
(hand-authored blocks `GATE_VALVE`, `CENTRIFUGAL_PUMP` with a `TAG` attribute,
|
||||
plain lines) pins that path.
|
||||
|
||||
### 9.4 Reader rules
|
||||
|
||||
- The reader never modifies the drawing.
|
||||
- It reads geometry from the drawing, never from the catalogue: a scaled or
|
||||
rotated INSERT is measured, not assumed.
|
||||
- Confidence is never raised by agreement between heuristics; it is the
|
||||
minimum of its inputs.
|
||||
|
||||
### 9.5 Deliverables (`engineering/pid/deliverables.py`)
|
||||
|
||||
| Tool | Row keys, in order |
|
||||
|---|---|
|
||||
| `pid_instrument_index` | `tag, func, loop, description, type, location, connected_to (comma-joined equipment/valve tags or line numbers), signal_lines (count), handle, confidence` |
|
||||
| `pid_line_list` | `line_number, line_class, size, service, spec, insulation, from_tag, from_port, to_tag, to_port, length_mm (rounded 1), handle, number_source` |
|
||||
| `pid_equipment_list` | `tag, kind, symbol, description, ports (count), connected_lines (count), unconnected_ports (comma-joined), handle, confidence` |
|
||||
|
||||
Rows are sorted by tag (natural sort: `P-2` before `P-10`), untagged rows last
|
||||
with `tag: null`.
|
||||
|
||||
## 10. `pid_from_spec`
|
||||
|
||||
Schema (validated with the same strictness as `block_define`; every error names
|
||||
the path, e.g. `lines[2].from`):
|
||||
|
||||
```json
|
||||
{"sheet": {"layer_set": "pid", "sheet_size": "A3", "scale": 1.0, "intent": "Feed section"},
|
||||
"equipment": [{"id": "P-101", "symbol": "centrifugal_pump", "x": 100, "y": 100, "rotation": 0,
|
||||
"tag": "P-101", "desc": "Feed pump"},
|
||||
{"id": "V-201", "symbol": "vertical_vessel", "x": 220, "y": 120,
|
||||
"params": {"width": 20, "height": 40, "nozzles": [{"name": "N1", "side": "top", "fraction": 0.5},
|
||||
{"name": "N2", "side": "left", "fraction": 0.3}]}}],
|
||||
"valves": [{"id": "HV-101", "symbol": "gate", "actuator": "hand", "x": 160, "y": 104, "tag": "HV-101"}],
|
||||
"instruments": [{"id": "FIC-101", "type": "dcs", "location": "primary", "x": 160, "y": 140, "tag": "FIC-101"}],
|
||||
"lines": [{"from": "P-101.discharge", "to": "HV-101.in", "class": "process_major", "number": "100-P-1001-CS1"},
|
||||
{"from": "HV-101.out", "to": "V-201.N2", "class": "process_major", "number": "100-P-1001-CS1"},
|
||||
{"from": "FIC-101", "to": "HV-101.signal", "class": "electric"}],
|
||||
"connectors": [{"id": "OP-1", "direction": "out", "x": 300, "y": 120, "tag": "TO P&ID-002", "link": "L-17",
|
||||
"from": "V-201.N1", "class": "process_major"}]}
|
||||
```
|
||||
|
||||
Behaviour: `drawing_plan` is called with the sheet block when no plan is held
|
||||
(so critique replays it); then, inside `transaction_begin`, symbols are
|
||||
inserted in the order equipment → valves → instruments → connectors, then lines
|
||||
(`id.port` references resolve to handles; an instrument reference without a
|
||||
port means its radial `signal` port). A connector with `direction: "out"`
|
||||
carries `from` (the line runs into the connector); one with `direction: "in"`
|
||||
carries `to` (the line runs out of it); each becomes one `pid_line_draw` call
|
||||
with the connector's single port. On any error the transaction is rolled
|
||||
back and the error names the failing item; on success the transaction is
|
||||
committed and the result is `{"handles": {id: handle}, "graph": pid_graph(),
|
||||
"critique": [pid issues], "crossings_total"}`. `dry_run=True` validates the
|
||||
spec and resolves the routing in memory, returning the planned vertices and
|
||||
crossings without touching the drawing.
|
||||
|
||||
## 11. Critique, refine, plan, finalize, prompt
|
||||
|
||||
### 11.1 Focuses (added to `CritiqueFocus` and `ALL_CRITIQUE_FOCUSES`)
|
||||
|
||||
| focus | severity | condition | handles / detail |
|
||||
|---|---|---|---|
|
||||
| `pid_dangling_line` | error | an edge end resolves to nothing | edge; `end`, `x, y`, `nearest` |
|
||||
| `pid_duplicate_tag` | error | two nodes carry the same full tag (instrument `FUNC-LOOP` including suffix; equipment tag) | both nodes; `tag` |
|
||||
| `pid_incompatible_connection` | error | a `signal`-class edge ends on a `process` port, or a process-class edge ends on a `signal` port | edge + node; `expected`, `actual` |
|
||||
| `pid_untagged_instrument` | warning | an instrument node with empty `FUNC` or `LOOP` | node |
|
||||
| `pid_illegal_tag` | warning | `pid_tag_parse` returns `valid: false` for a node's tag | node; `errors` |
|
||||
| `pid_unconnected_equipment` | info | an equipment/valve node with zero edges on every port | node |
|
||||
|
||||
All six build one `pid_graph(include_foreign=True)` per critique run (cached
|
||||
within the run) and return `[]` when `stats["nodes_by_kind"]` is empty, so a
|
||||
drawing without P&ID content is never scored on these rules. They run under
|
||||
`focus=None` and therefore inside `drawing_finalize`.
|
||||
|
||||
### 11.2 Refine hints (`engineering/refiner.py` registry)
|
||||
|
||||
No deterministic repair moves geometry. `pid_dangling_line` → hint naming the
|
||||
nearest port and the `pid_line_draw` call that would connect it;
|
||||
`pid_duplicate_tag` / `pid_illegal_tag` / `pid_untagged_instrument` → hint
|
||||
naming the node and `block_set_attributes`; `pid_incompatible_connection` →
|
||||
hint to redraw with the right class; `pid_unconnected_equipment` → informational.
|
||||
|
||||
### 11.3 Plan
|
||||
|
||||
`detect_ambiguities` gains one rule: intent text matches
|
||||
`\b(p&id|pid|piping and instrumentation|process flow|instrument)\b` (case-
|
||||
insensitive) while `layer_set_id != "pid"` → clarification "intent reads as a
|
||||
P&ID but the layer set is `<id>`; pass `layer_set_id='pid'`".
|
||||
|
||||
### 11.4 Prompt
|
||||
|
||||
`prompt_pid_diagram(project_name, revision)` is rewritten to the tool
|
||||
workflow: `drawing_plan(layer_set_id="pid")` → `drawing_apply_iso_layers("pid")`
|
||||
→ `pid_symbol_insert` per equipment/valve/instrument (tags in ISA-5.1 form) →
|
||||
`pid_line_draw` port-to-port (never free points for equipment connections) →
|
||||
`pid_graph` and read `dangling`/`confidence_min` → `drawing_critique(focus=None)`
|
||||
must be empty → `drawing_finalize`. It states the reference sizes (section 4.2)
|
||||
and that `pid_from_spec` does the whole sheet in one call.
|
||||
|
||||
### 11.5 Validator
|
||||
|
||||
No change: none of the eight steps assumes mechanical content. A test runs
|
||||
`drawing_finalize` on the spec example and asserts the score is computed and
|
||||
≥ 90 with zero P&ID issues.
|
||||
|
||||
## 12. Tool packs (`TOOL_PACKS`)
|
||||
|
||||
- `config.settings.tool_packs: str = "all"`; parsed as a comma list, trimmed,
|
||||
lower-cased. Valid packs in 1.6.0 after this track: `core`, `pid` (later
|
||||
tracks add `mech`, `arch`, `plant3d`, `settings`). `core` is every registered
|
||||
tool not claimed by another pack and cannot be disabled; an unknown pack name
|
||||
logs a warning, is reported in `system_about["tool_packs"]["ignored"]`, and
|
||||
is otherwise ignored (the server boots).
|
||||
- `PACK_TOOL_NAMES: dict[str, frozenset[str]]` next to `LEAN_TOOL_NAMES`;
|
||||
`_profile_enabled_names` becomes `enabled = registered ∩ packs_enabled`,
|
||||
then `∩ LEAN_TOOL_NAMES` for `lean`, then the `ENABLE_3D` subtraction.
|
||||
- `LEAN_TOOL_NAMES` gains `pid_symbol_insert`, `pid_line_draw`, `pid_graph`
|
||||
(lean = 50).
|
||||
- `system_about` reports `tool_packs: {"enabled": [...], "available": [...],
|
||||
"ignored": [...]}`; `_active_tool_profile` records the same. The README token
|
||||
table gains a row per pack (measured with `benchmarks/token_suite.py`).
|
||||
- The `unknown_tool` rejection message names `TOOL_PACKS` alongside
|
||||
`TOOL_PROFILE` / `ENABLE_3D`.
|
||||
|
||||
## 13. Resources
|
||||
|
||||
- `autocad://pid/symbols` — the `pid_symbol_list()` payload.
|
||||
- `autocad://standards/isa51` — the letter tables and grammar summary from
|
||||
`tags.py`, so a model can read the standard instead of guessing.
|
||||
|
||||
Both are static, cost nothing in the tool budget, and are added to
|
||||
`docs/tool-inventory.json` (resources section) by the generator.
|
||||
|
||||
## 14. Error handling summary
|
||||
|
||||
- Validation errors are `ValueError`/`TypeError` raised **before** any write,
|
||||
with the offending key or index in the message; the server wrapper turns them
|
||||
into `ToolError` as it does today.
|
||||
- Engine boundaries: none in this track — every method is abstract on both
|
||||
engines. (`drawing_open` of a DWG headlessly stays the existing refusal.)
|
||||
- Reader results carry `source`/`confidence` fields instead of raising on
|
||||
uncertainty; only a failure to read the drawing at all raises.
|
||||
- Nothing in `pid_*` calls `system_run_command` / `system_run_lisp`.
|
||||
|
||||
## 15. Testing and evidence
|
||||
|
||||
Unit (headless, CI):
|
||||
|
||||
- `tests/test_pid_symbols.py` — for **every** catalogue entry and every
|
||||
generated variant: primitives pass `block_define` validation, declared
|
||||
`bbox` contains every primitive, every port lies on the bbox boundary (or at
|
||||
the centre for radial ports), ≥ 1 port, ATTDEF tags unique, name matches
|
||||
the pattern, `source` non-empty; parametric hash stability; actuator
|
||||
composition adds exactly one `signal` port.
|
||||
- `tests/test_block_define.py` — ezdxf: definition, ATTDEF count, insert with
|
||||
attributes, overwrite semantics (existing INSERT shows new geometry),
|
||||
refusals (empty, bad spec index, name clash, anonymous name); COM: the
|
||||
fake-ActiveX pattern from `test_block_from_entities.py` asserting the
|
||||
`Blocks.Add` / `AddAttribute` call sequence and the invisible mode flag.
|
||||
- `tests/test_xdata.py` — both engines: typing by group code, round trip,
|
||||
removal on empty list, 255-char and 16 KB refusals, APPID registration,
|
||||
chunked JSON round trip through `pid/xdata.py`.
|
||||
- `tests/test_pid_lines.py` — pure routing: straight/L/Z/U selection, stub
|
||||
honoured, no-candidate refusal, waypoint validation, upside-down label
|
||||
angle correction, crossing count against an existing line, marker placement
|
||||
on segments ≥ 15 mm only, class → layer/linetype table, line number format
|
||||
with empty fields, `seq` auto-increment.
|
||||
- `tests/test_pid_tags.py` — the JSON fixture (≥ 40 valid / ≥ 10 invalid) plus
|
||||
the disambiguation rules one by one; equipment prefix override.
|
||||
- `tests/test_pid_graph.py` — a P&ID drawn with the tools: expected node kinds,
|
||||
edge count, zero dangling; nudge one symbol by 1 mm (> tolerance) →
|
||||
`dangling` and `nearest` reported; a branch line onto a segment → junction;
|
||||
scale 2 / rotation 90 INSERT → ports measured correctly; foreign fixture →
|
||||
heuristic nodes at 0.6, inferred ports, `confidence_min < 1`; a title block
|
||||
INSERT is not a node; `scope="all"` across two layouts; off-page link pairing.
|
||||
- `tests/test_pid_deliverables.py` — row order and keys, natural sort, CSV
|
||||
written under `ALLOWED_PATHS` and refused outside.
|
||||
- `tests/test_pid_critique.py` — each focus positive and negative; all six
|
||||
return `[]` on the README gear sheet; `drawing_refine` emits hints and moves
|
||||
nothing.
|
||||
- `tests/test_pid_from_spec.py` — the section 10 example end to end; a bad
|
||||
reference rolls back (entity count unchanged); `dry_run` writes nothing;
|
||||
`drawing_finalize` score ≥ 90.
|
||||
- `tests/test_tool_packs.py` — `TOOL_PACKS=core` hides every `pid_*` tool and
|
||||
keeps `block_define`; unknown pack ignored with `ignored`; `core` cannot be
|
||||
disabled; interaction with `lean` and `ENABLE_3D`.
|
||||
- Existing gates extended: alias records for 12 tools (`test_discovery_aliases`),
|
||||
`docs/tool-inventory.json` regenerated (`test_tool_inventory`), README
|
||||
snapshot line and `SECTION 17: P&ID (9 tools)` header
|
||||
(`test_release_consistency`), capability maps unchanged
|
||||
(`test_capability_contract`).
|
||||
|
||||
Render evidence: `scripts/render_pid_catalog.py` renders every base symbol and
|
||||
the 20 bubble variants to `docs/assets/pid-catalog.png` (contact sheet, labelled)
|
||||
and `scripts/render_readme_pid.py` renders the section 10 example to
|
||||
`docs/assets/pid-showcase.png`; a test asserts each render is non-blank.
|
||||
|
||||
Live COM: `scripts/smoke_pid_com.py` runs `block_define` → `pid_symbol_insert`
|
||||
→ `entity_set/get_xdata` → `pid_line_draw` → `pid_graph` in a **new** document
|
||||
on the running AutoCAD (never the operator's open drawing), prints the graph
|
||||
stats, and its output plus the AutoCAD version go into the CHANGELOG entry. If
|
||||
no AutoCAD is reachable the script exits 2 and says so; the changelog then
|
||||
states the COM paths are fake-tested only.
|
||||
|
||||
Benchmark: `benchmarks/tasks_v4.py = TASKS_V3 + ("pid_roundtrip", "pid",
|
||||
"Draw a three-equipment P&ID and read it back with matching index and line
|
||||
list", 1.0)`; `run_competitors --matrix v4`; competitor reports stay unfilled
|
||||
(`not run`). Correctness suite +3: `pid_block_define_attdef_roundtrip`,
|
||||
`pid_tag_parse_fic`, `pid_graph_edge_count` — all `miss → pass` against
|
||||
v1.5.1.
|
||||
|
||||
## 16. Documentation and gates to update
|
||||
|
||||
- `CLAUDE.md`: section "17. P&ID (9 tools)", the two generic contracts, the
|
||||
`TOOL_PACKS` variable, the reader rules (section 9.4) in Key Conventions.
|
||||
- `README.md`: "What you get" row for P&ID, the catalogue contact sheet, the
|
||||
token table row per pack, `Configuration` entry for `TOOL_PACKS`, snapshot
|
||||
line (166 tools, 8 resources).
|
||||
- `CHANGELOG.md`: `Unreleased` → 1.6.0 entry for track A with the measured
|
||||
refusals and the live COM smoke record.
|
||||
- `.env.example`: `TOOL_PACKS`.
|
||||
- `docs/tool-inventory.json`: regenerated.
|
||||
|
||||
## 17. Milestones (input to the implementation plan)
|
||||
|
||||
1. Contracts: `block_define` + XDATA on both engines, tests, aliases, inventory.
|
||||
2. Catalogue: `symbols.py` + `geometry.py`, catalogue tests, contact-sheet script.
|
||||
3. `pid_symbol_list/insert`, layers, XDATA payloads.
|
||||
4. `lines.py` + `pid_line_draw`, markers, arrows, labels.
|
||||
5. `tags.py` + `pid_tag_parse` + fixture + resource.
|
||||
6. `graph.py` + `pid_graph` (+ foreign fixture), deliverables.
|
||||
7. Critique focuses, refine hints, plan rule, prompt rewrite, finalize test.
|
||||
8. `pid_from_spec`, README showcase render.
|
||||
9. Tool packs, `system_about`, token table.
|
||||
10. Benchmark v4 task, correctness checks, live COM smoke, docs, gates green.
|
||||
@@ -0,0 +1,235 @@
|
||||
# AutoCAD MCP Pro v1.6.0 — Roadmap and decomposition
|
||||
|
||||
**Date:** 2026-09-15 · **Status:** decomposition approved; every track below gets its
|
||||
own design spec and implementation plan before code · **Baseline:** v1.5.1
|
||||
(154 registered tools, 19 groups, 1369 tests)
|
||||
|
||||
## 1. Goal
|
||||
|
||||
An LLM driving this server can — without guessing coordinates, to a named
|
||||
standard, and with the result verified by the server itself —
|
||||
|
||||
- (A) draw a **P&ID** and read one back as a graph, instrument index and line list;
|
||||
- (B) produce a complete **machine-part / assembly sheet** (views, sections,
|
||||
standard parts, symbols, BOM, balloons);
|
||||
- (C/H) **understand** an existing or foreign drawing (topology, standards,
|
||||
revision diff);
|
||||
- (D) query an **AutoCAD Plant 3D** project read-only;
|
||||
- (E) control **AutoCAD's own settings** (styles, page setup, templates,
|
||||
preferences, documents);
|
||||
- (F) draft an **architectural** floor plan and read rooms back from one.
|
||||
|
||||
The bar is "best in every category, and evidenced" — not the longest checklist.
|
||||
Features ship when their contracts and limitations are testable (README,
|
||||
"Roadmap (1.6)").
|
||||
|
||||
### Success criteria (measured, not asserted)
|
||||
|
||||
| # | Criterion | Where it is measured |
|
||||
|---|---|---|
|
||||
| 1 | `pid_roundtrip`: a three-equipment P&ID drawn with the tools is read back by `pid_graph` with the expected edge count, zero dangling lines, a correct instrument index and line list; `drawing_critique` returns 0 issues; `drawing_finalize` score ≥ 90 | new benchmark task + correctness suite |
|
||||
| 2 | A foreign P&ID (plain blocks + `TAG` attributes, no catalog) yields a graph whose every node carries a confidence < 1.0 — no fabricated certainty | unit test on a hand-authored fixture |
|
||||
| 3 | `mech_assembly`: a flange-coupling / bearing-housing style sheet (views + section + BOM + balloons + ISO 129 dims) from generators; critique 0 issues; score ≥ 90 | new benchmark task |
|
||||
| 4 | Topology DRC parity with beiming v4 (`DANGLING_ENDPOINT`, `NEAR_MISS`, `INTERIOR_CROSSING`) and `drawing_diff` on two revisions of the README hero sheet | tests + benchmark task |
|
||||
| 5 | Plant 3D line list / BOM / weld list correct on a synthetic `.dcf` fixture built from the public projects' schema, and manually checked against the three public projects | tests + `scripts/verify_plant3d_public.py` |
|
||||
| 6 | Correctness A/B vs v1.5.1: zero regressions (`python benchmarks/compare_versions.py v1.5.1`) | release gate |
|
||||
| 7 | Headless creation throughput back to v1.4.0 numbers (timeout armed only where a call can block) | perf lane |
|
||||
| 8 | Re-score on the 100-point rubric in `benchmarks/source_review.json` ≥ 90 **with evidence per category**, published next to the competitor rows | docs |
|
||||
|
||||
## 2. Competitive research (snapshot 2026-09-14)
|
||||
|
||||
Method: `gh search repos` / `gh api` over GitHub, 20 READMEs read in full
|
||||
(Firecrawl was unavailable — HTTP 402). Stars as of the snapshot.
|
||||
|
||||
| Repository | ★ | What it has that v1.5.1 does not |
|
||||
|---|---:|---|
|
||||
| puran-water/autocad-mcp v3.1 | 506 | `pid` tool (`insert_valve/pump/tank/instrument`, `draw_process_line`, `connect_equipment`, `add_line_number`) — bound to the **commercial** CAD Tools Online symbol library at `C:\PIDv4-CTO\`; 8 consolidated tools; freehand AutoLISP |
|
||||
| LokmenoWer/best-cad-mcp v1.6 | 16 | 322 tools (profiles 114/215/322). **Drawing understanding**: CAD-IR, semantic objects and graphs, dimension binding, constraint checks; CADPlan dry-run; BOM + balloons; image-to-CAD tracing; VLM grounding with pixel/world mapping. COM-only |
|
||||
| beiming183-cloud/AutoCAD-MCP v4.0 + AutoCAD-skills | 7 + 7 | Native .NET transaction worker; topology DRC (`DANGLING_ENDPOINT`, `NEAR_MISS_CONNECTION`, `INTERIOR_CROSSING`), view-source consistency, plot-scale truth; product/industrial features; the skill expects threads, springs, bearings, splines |
|
||||
| varavista/autocad-mcp v5.0 | — | `xref` (attach/detach/reload/bind), `validate` (connectivity, orphans, duplicates, qc_report), `export` (BOM, data_extract, Excel), `electrical` (NEC calcs, panel schedule, wire numbers), `titleblock_fill`, `batch_plot`, `page_setup`, natural-language commands |
|
||||
| john0909/ZWCAD-Mechanical-MCP | 13 | 37 tools: `manage_bom` / `create_partlist`, `insert_balloon`, `manage_frame`, `manage_title_block(set_field)`, `query_dimensions`, `manage_style` (layer/linetype/textstyle/dimstyle), `manage_xdata`, `manage_dictionary`, `manage_document` |
|
||||
| Romeraaa/autocadplant3d-mcp | 1 | Plant 3D project queried **read-only through its SQLite files** (`.dcf`, `.pspc`): line list, component list, BOM, pipe lengths, weld list, bolt/gasket list, `validate_specs`, `find_untagged` — no .NET plugin, no open session |
|
||||
| AnCode666/multiCAD-mcp | — | Multi-CAD COM (AutoCAD/ZWCAD/GstarCAD/BricsCAD), multi-document switching, Excel export |
|
||||
| thepiruthvirajan/autocad-mcp-server | 62 | Walls, doors, windows, building structures over COM |
|
||||
| xstaar/autocad-mcp | 6 | YQArch architectural add-on, 684 commands |
|
||||
| OmniZenRaj/dxf-mcp | 0 | Read-only DXF: BOM from block attributes, `check_standards` (required/forbidden layers, units) |
|
||||
| mattmohandiss/cad-mcp-server | 5 | STEP inspection, DFM checks, `diff_step` revision compare (OCCT) |
|
||||
| daobataotie/CAD-MCP | 547 | Basic COM drawing over three CAD brands; NL command parsing — stars, not capability |
|
||||
| mixelpixx/KiCAD-MCP-Server | 2246 | Not AutoCAD; the most-starred CAD MCP, shows the ceiling of the category |
|
||||
|
||||
P&ID ecosystem outside MCP:
|
||||
|
||||
- **Coldbari/IPD-Studio** (25★) defines "intelligent P&ID": ISA-5.1 tag parsing
|
||||
(`FIC` → Flow Indicating Controller), loop derivation, instrument index and
|
||||
line list *generated from the model*, 16 line classes, off-page connectors,
|
||||
live validation (duplicate/missing/illegal tags, dangling lines, incompatible
|
||||
connections), DEXPI-shaped XML, R12 DXF export.
|
||||
- **process-intelligence-research/pyDEXPI** (222★) — DEXPI 1.3 data model in
|
||||
Pydantic, Proteus XML loader, NetworkX graph. **AGPL-3.0**: cannot be a
|
||||
dependency of an MIT server. DEXPI export is therefore out of 1.6.
|
||||
- **Public Plant 3D projects with real `.dcf` / `.pspc` files**:
|
||||
`Aggie-Propulsion-Lab/Mu-Propellant-Feed` (Piping.dcf 397 KB, ProcessPower.dcf
|
||||
1.0 MB, 30 spec sheets), `bnguyen07/AutoCAD` ("Project 1"),
|
||||
`marianabucsa/University` (Lab10). None carries a licence, so they are
|
||||
**schema sources only** — never committed; tests run on a synthetic fixture
|
||||
generated by our own script with the same schema.
|
||||
|
||||
Today's P&ID support in v1.5.1 is a prompt template (`prompt_pid_diagram`) that
|
||||
tells the model to hand-draw pumps as circle + triangle — the "no coordinate
|
||||
guessing" rule, violated by our own prompt.
|
||||
|
||||
## 3. Tracks
|
||||
|
||||
Each track: scope · not in 1.6 · headline evidence. Detailed designs live in
|
||||
their own specs (`2026-MM-DD-v1.6-<track>-design.md`).
|
||||
|
||||
### A · P&ID — expert and reader (`engineering/pid/`)
|
||||
|
||||
- Symbol catalogue authored **in code** (ISO 10628-2 equipment and valves,
|
||||
ISA-5.1 instrumentation): ~41 base symbols, valve × actuator variants, 4 × 5
|
||||
instrument-bubble variants, parametric vessels; every symbol a real block
|
||||
with `TAG` attributes and named **ports** (position + direction).
|
||||
- Two new generic contracts: `block_define(name, entities, attdefs)` and
|
||||
`entity_get/set_xdata`.
|
||||
- `pid_symbol_list/insert`, `pid_line_draw` (port-to-port orthogonal routing,
|
||||
ISA-5.1 line classes → layer/linetype/marker, line numbers), `pid_tag_parse`
|
||||
(ISA-5.1 grammar), `pid_graph`, `pid_instrument_index`, `pid_line_list`,
|
||||
`pid_equipment_list`, `pid_from_spec`; critique focuses `pid_*`;
|
||||
`prompt_pid_diagram` rewritten onto the tools.
|
||||
- Not in 1.6: DEXPI export, ISA-5.2 logic symbols, obstacle-avoiding routing,
|
||||
cross-file off-page links (same-file layouts only).
|
||||
- Evidence: criteria 1 and 2.
|
||||
|
||||
### E · AutoCAD settings and environment
|
||||
|
||||
- `dimstyle_*`, `textstyle_*`, `mleaderstyle_*`, `tablestyle_*` (ISO-25 /
|
||||
ISOCPEUR authored, both engines); `drawing_settings` extended (limits, grid,
|
||||
snap, ortho, polar, LTSCALE/PSLTSCALE, CANNOSCALE, INSUNITS/LUNITS/AUNITS)
|
||||
over a **validated whitelist** plus `system_variable_describe`.
|
||||
- `page_setup_*` with CTB selection, `batch_plot`; `.dwt` templates
|
||||
(`drawing_new(template=)`, bundled ISO A3-mech / A1-arch / A3-P&ID).
|
||||
- COM-only, refused headlessly: `system_preferences_get/set`,
|
||||
`drawing_properties` (DWGPROPS), `document_list/activate/close`,
|
||||
`system_launch`. Both engines: `layer_state_*`, `view_named_*`, `ucs_*`.
|
||||
- Evidence: settings round-trip tests both engines; a page-setup PDF whose
|
||||
mediabox is read back (beiming's "plot-scale truth").
|
||||
|
||||
### B · Mechanical 2D — the complete part drawer (`engineering/mech/`)
|
||||
|
||||
- Standard parts as generators: ISO 4014/4017 bolts, ISO 4032 nuts, ISO 7089
|
||||
washers, ISO 4762 cap screws, ISO 6410 thread representation, DIN 471/472
|
||||
ring grooves, DIN 509 undercuts, DIN 332 centre holes, ISO 15 / ISO 8826
|
||||
bearings, ISO 3601 O-ring grooves.
|
||||
- Part generators from one parameter source: `shaft_draw` (steps, chamfers,
|
||||
fillets, keyways, threads, grooves → front view + section + auto dims),
|
||||
`plate_draw` (outline + holes/slots/pockets → front + side, hidden lines for
|
||||
through features), `flange_draw`; `mech_part_from_spec` does the whole sheet
|
||||
in one call.
|
||||
- Annotation symbols: surface texture ISO 21920-1 / ISO 1302, welding ISO 2553,
|
||||
centre marks and centrelines, section line A-A, detail view; material hatch
|
||||
map (ISO 128-50).
|
||||
- BOM / parts list ISO 7573 with balloons; `bom_extract` from block attributes;
|
||||
`data_extract` → CSV/XLSX.
|
||||
- Not in 1.6: a general 3D → 2D projection kernel (views come from generators,
|
||||
not from arbitrary solids), splines DIN 5480, springs.
|
||||
- Evidence: criterion 3.
|
||||
|
||||
### G · Sheet and delivery standard
|
||||
|
||||
- `sheet_frame_iso5457(size)` with zone letters, centring and trimming marks;
|
||||
A4–A0 title blocks (ISO 7200); revision block + `revision_add` (with
|
||||
revcloud); `xref_attach/bind/reload/detach`; `image_attach` (raster
|
||||
underlay); headless real DWG through `odafc` when the ODA converter is
|
||||
installed, refused otherwise.
|
||||
- Evidence: frame geometry pinned against ISO 5457 tables; xref round-trip.
|
||||
|
||||
### F · Architectural 2D (`engineering/arch/`)
|
||||
|
||||
- `arch_grid`, `arch_wall` (thickness, justification, corner clean-up),
|
||||
`arch_door` (opens the wall, leaf + swing), `arch_window`, `arch_stair`,
|
||||
`arch_room_label` (area measured, not typed), `arch_north_arrow`,
|
||||
`arch_section_mark`, `arch_schedule(doors|windows|rooms)`; furniture and
|
||||
sanitary block catalogue over `block_define`; ISO 13567 layers (existing),
|
||||
ISO 4157 numbering, ISO 7519 / ISO 128-23 line rules, annotative text at
|
||||
1:50 / 1:100.
|
||||
- Reader: `arch_room_detect` — rooms and an area schedule from a foreign plan,
|
||||
over `boundary_trace`.
|
||||
- Not in 1.6: AutoCAD Architecture / Revit objects, 3D walls, IFC.
|
||||
- Evidence: a two-room plan drawn, read back, schedule matches.
|
||||
|
||||
### H · Understanding and QA
|
||||
|
||||
- Topology DRC as critique focuses: `dangling_endpoint`, `near_miss`,
|
||||
`interior_crossing`, `overlap`, `zero_length`; `check_standards`
|
||||
(required/forbidden layers, units, text heights, dim styles);
|
||||
`query_dimensions`; `drawing_scan` (view clusters, title block, tables);
|
||||
`drawing_diff(a, b)` by handle and geometry digest.
|
||||
- Evidence: criterion 4.
|
||||
|
||||
### D · Plant 3D, read-only (`plant3d/`)
|
||||
|
||||
- SQLite reads of `.dcf` / `.pspc`: `plant3d_list_projects/detect_project`,
|
||||
`line_list`, `component_list`, `bom`, `pipe_length`, `weld_list`,
|
||||
`bolt_gasket_list`, `validate_specs`, `find_untagged`. Never writes.
|
||||
- Evidence: criterion 5.
|
||||
|
||||
### Cross-cutting (inside the tracks above)
|
||||
|
||||
- **I · Speed** — per-call timeout armed only where a call can block; `*_from_spec`
|
||||
one-call sheets; perf lane re-measured (criterion 7).
|
||||
- **K · Human in the loop** — `user_pick_point`, `user_select` (COM `GetPoint` /
|
||||
`GetSelection`), `system_prompt_message`; refused headlessly.
|
||||
- **L · Knowledge resources** — MCP resources for authored standard tables
|
||||
(`autocad://standards/iso286`, `isa51_letters`, `layers/{set}`, `pid/symbols`):
|
||||
zero tool-budget cost.
|
||||
|
||||
## 4. Order and dependencies
|
||||
|
||||
**A → E → B → G → F → H → D.**
|
||||
|
||||
- A first: the loudest gap (three rivals ship `pid`, we ship a prompt), and it
|
||||
builds the block/port/graph infrastructure F reuses.
|
||||
- E second, small: B and F need ISO dimstyles, text styles and templates.
|
||||
- B before F: the existing engineering package, critique and score are
|
||||
mechanical-shaped — proven pattern, deepened.
|
||||
- G before F: frames, title blocks and revision block serve every sheet.
|
||||
- H after the drawing tracks so the DRC has three domains to run against.
|
||||
- D last: isolated, read-only, no shared code.
|
||||
|
||||
Each track = its own spec → plan → implementation → CHANGELOG entry. 1.6.0 is
|
||||
released once every track's criterion is green; pre-release tags (`1.6.0a1`…)
|
||||
after each track are optional and decided at that point.
|
||||
|
||||
## 5. Cross-cutting decisions
|
||||
|
||||
1. **Tool packs.** `TOOL_PACKS` (comma list; default `all`) with packs `core`,
|
||||
`mech`, `pid`, `arch`, `plant3d`, `settings`. A client advertises only the
|
||||
packs it uses; `TOOL_PROFILE` filters within enabled packs; `DISCOVERY_MODE`
|
||||
indexes every enabled pack; `system_about` reports packs; idle token cost is
|
||||
published per pack. Rationale: ~300 tools would push `full` to ~80k idle
|
||||
tokens — best-cad-mcp's most criticised trait.
|
||||
2. **Dual-engine by default; `@capability` only for real engine boundaries**
|
||||
(unchanged rule). Every COM path added in 1.6 is executed once against the
|
||||
live AutoCAD on the author's machine and recorded in the changelog; "written
|
||||
against the ActiveX reference, never executed" is no longer an acceptable
|
||||
final state where AutoCAD is available.
|
||||
3. **Every tool** has an alias-corpus entry, a docstring, tests on both engines
|
||||
(fake-ActiveX for COM), and refusals that were measured rather than assumed.
|
||||
4. **Authored standards data is pinned by provenance tests** (the `fits.py`
|
||||
pattern): symbol geometry, ISA letter tables, ISO 5457 frames, thread
|
||||
tables — each with a test a reader can check against the standard.
|
||||
5. **Benchmarks.** Each track adds a task to the matrix; competitor reports are
|
||||
never back-filled (chart shows `not run`). The correctness suite grows per
|
||||
track and the A/B gate runs against v1.5.1.
|
||||
6. **Domain packs must not degrade other domains**: every new critique focus
|
||||
returns nothing on a drawing with no entities of its domain, so a mechanical
|
||||
sheet is not scored on P&ID rules and vice versa.
|
||||
7. **No licence contamination**: pyDEXPI (AGPL) and the CTO library
|
||||
(commercial) are excluded; public Plant 3D projects are schema sources only.
|
||||
|
||||
## 6. Explicitly out of 1.6
|
||||
|
||||
Electrical schematics IEC 60617 (**1.7** — cheap once A's symbol/graph
|
||||
infrastructure exists), DEXPI export, image-to-CAD tracing, a STEP/3D kernel,
|
||||
a bundled VLM judge, BricsCAD/ZWCAD/GstarCAD verification, Plant 3D writes,
|
||||
ISA-5.2 logic symbols, Revit/IFC.
|
||||
Reference in New Issue
Block a user