diff --git a/.claude/skills/autocad-mcp-premium/SKILL.md b/.claude/skills/autocad-mcp-premium/SKILL.md deleted file mode 100644 index 3c8fc49..0000000 --- a/.claude/skills/autocad-mcp-premium/SKILL.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -name: autocad-mcp-premium -description: Use when working with AutoCAD MCP Pro tools and the user asks for technical drawings, P&ID, mechanical engineering output, ISO-conformant drawings, premium-quality CAD, or mentions trim/extend/fillet/chamfer, layer discipline, or "premium drawing". Apply the plan→draw→critique workflow strictly. ---- - -# AutoCAD MCP Premium Drafting Skill - -Premium drafting discipline for the AutoCAD MCP Pro server. The goal: ISO-conformant -output that a real draftsman would sign. The path: a strict workflow + the corner -ops + meta-tool layer that ship with the server. - -## When this skill applies - -- The user asks for a **technical drawing**, **P&ID**, **mechanical part**, or - any drawing intended for production / fabrication / approval. -- The user mentions **TRIM, EXTEND, FILLET, CHAMFER**, **OSNAP**, **ISO 128/129/ - 13567**, **layer discipline**, or **premium drawing**. -- You are about to call `entity_create_*` for a deliverable drawing — **stop and - apply the workflow below first**. - -## The 9-step Premium Workflow (non-negotiable) - -```text -1. drawing_plan(intent, sheet_size, scale) # commit intent BEFORE drawing -2. drawing_apply_iso_layers("mech" | "pid" | "iso13567") -3. construction_xline(...) # scaffolding (optional) -4. entity_create_line / circle / arc / ... # geometry on engineering layers -5. entity_trim / entity_fillet / entity_chamfer # close corners explicitly -6. handles = entity_select_smart({type, layer, ...}) # don't memorise handles -7. dimension_auto(handles, style="chain"|"baseline"|"ordinate") -8. issues = drawing_critique(focus=None) # must be [] -9. construction_clear() -10. drawing_finalize(save_path=..., screenshot_path=...) -``` - -If any of these is skipped, the drawing is **not premium** — it's a sketch. - -## The 8 Non-Negotiable Rules (mirror of CLAUDE.md) - -1. **Plan before draw** — `drawing_plan` first; PlanSpec is the contract. -2. **No coordinate guessing** — `point_from_snap(handle, snap, ref_x, ref_y)`. -3. **Layer discipline** — engineering layers only; CONSTRUCTION for scaffold. -4. **Lineweights are ISO 128** — bootstrap via `drawing_apply_iso_layers`. -5. **Corners are explicit** — `entity_trim` / `entity_fillet` / `entity_chamfer`. -6. **Dimensions via auto** — `dimension_auto`, not loops of `dimension_linear`. -7. **Critique then finalize** — `drawing_critique` must return `[]`. -8. **Meta-tools over raw** — never reimplement what a meta-tool already does. - -## Tool Inventory (this skill assumes server has these registered) - -| Group | Tools | Count | -|-------|-------|-------| -| Drawing | `drawing_*` (info/new/open/save/export/finalize/...) | 11 | -| Entity Create | `entity_create_line/circle/arc/polyline/text/...` | 12 | -| Dimensions | `dimension_linear/aligned/angular/radius/diameter` | 5 | -| Entity Modify | `entity_move/copy/rotate/scale/mirror/offset/delete/...` | 10 | -| **Corner Ops** ⭐ | `entity_trim` `entity_extend` `entity_fillet` `entity_chamfer` | **4** | -| Entity Query | `entity_get/list/delete_many` | 3 | -| Layer Mgmt | `layer_*`, `linetype_list/load` | 14 | -| Block Ops | `block_*` | 7 | -| Analysis | `analysis_*` | 8 | -| View | `view_*` | 4 | -| Transactions | `transaction_*` | 3 | -| System | `system_*` | 6 | -| Engineering | `gear_*`, `keyway_*`, `titleblock_*`, `drawing_finalize` | 7 | -| **Premium Meta** ⭐ | `drawing_plan` `drawing_critique` `point_from_snap` `construction_xline/clear` `drawing_apply_iso_layers` `dimension_auto` `entity_select_smart` | **8** | - -## Reference Documents (read on demand) - -- [`references/corner-ops.md`](references/corner-ops.md) — trim/extend/fillet/chamfer - semantics, decision tree (when to use which), corner-case table. -- [`references/meta-planning.md`](references/meta-planning.md) — `drawing_plan`, - `drawing_critique`, `point_from_snap` deep dive + PlanSpec / Issue schema. -- [`references/iso-standards.md`](references/iso-standards.md) — ISO 128 line - widths, ISO 13567 layer naming, ISO 129 dimension rules — printable tables. -- [`references/premium-workflow.md`](references/premium-workflow.md) — full - worked examples of the 9-step workflow with code blocks. - -## Examples - -- [`examples/premium-shaft.md`](examples/premium-shaft.md) — Shaft with keyway - (Anka-Makine reference part). -- [`examples/pid-simple-loop.md`](examples/pid-simple-loop.md) — P&ID flow - control loop with ISA-5.1 tags. - -## Anti-patterns to refuse - -- Calling `entity_create_*` before `drawing_plan` for a deliverable drawing. -- Computing endpoint/midpoint/intersection coordinates by hand instead of - `point_from_snap`. -- Drawing on layer 0 (or any non-engineering layer). -- Leaving the CONSTRUCTION layer populated at finalize time. -- Hand-rolling trim/extend math when `entity_trim` / `entity_extend` exists. -- Skipping `drawing_critique` and going straight to `drawing_finalize`. - -When the user requests a premium drawing, **walk the 9 steps in order** and -narrate each call. If a critique returns non-empty issues, fix them in place -before proceeding to finalize. diff --git a/.claude/skills/autocad-mcp-premium/examples/pid-simple-loop.md b/.claude/skills/autocad-mcp-premium/examples/pid-simple-loop.md deleted file mode 100644 index 05320f3..0000000 --- a/.claude/skills/autocad-mcp-premium/examples/pid-simple-loop.md +++ /dev/null @@ -1,107 +0,0 @@ -# Example — Simple P&ID Flow Control Loop - -ISA-5.1 tagged flow loop: pump P-101 → flow transmitter FT-101 → control valve -FCV-101 → vessel V-201. Drawn on A3 with the `pid` layer set. - -## Layout - -```text - ┌────┐ ╭──╮ ┌────────┐ - │P101├──────────────────┤FT├────────╳──────│ V-201 │ - └────┘ PROCESS-MAIN ╰┬─╯ PROCESS │ │ │ - │ signal │FCV │ │ - ↓ │101 │ │ - (FIC101) │ └────────┘ - controller │ - ─┴─ control output -``` - -## Full workflow - -```python -# 1. PLAN -plan = await drawing_plan( - intent="Flow control loop 101: P-101 → FT-101 → FCV-101 → V-201", - sheet_size="A3", - scale=1.0, - layer_set_id="pid", - dim_style="ordinate", # P&ID typically minimal dim - notes=["ISA-5.1 tag conventions", "Pneumatic signal lines dashed"], -) - -# 2. P&ID LAYERS -await drawing_apply_iso_layers("pid") - -# 3. EQUIPMENT — pump (circle) and vessel (rectangle) -pump = await entity_create_circle(40, 60, 6, layer="PROCESS-EQUIPMENT") -await entity_create_text("P-101", 33, 50, height=3.0, layer="INSTRUMENT-TAG-TEXT") - -vessel = await entity_create_rectangle(180, 40, 230, 80, layer="PROCESS-EQUIPMENT") -await entity_create_text("V-201", 195, 60, height=3.5, layer="INSTRUMENT-TAG-TEXT") - -# 4. PROCESS LINE — main horizontal (heavy) -main_line_a = await entity_create_line(46, 60, 110, 60, layer="PROCESS-PIPING-MAIN") -main_line_b = await entity_create_line(110, 60, 150, 60, layer="PROCESS-PIPING-MAIN") -main_line_c = await entity_create_line(150, 60, 180, 60, layer="PROCESS-PIPING-MAIN") - -# 5. INSTRUMENT BUBBLES — flow transmitter, control valve -ft = await entity_create_circle(110, 60, 4, layer="INSTRUMENT-SYMBOL") -await entity_create_text("FT", 107, 60.5, height=2.0, layer="INSTRUMENT-TAG-TEXT") -await entity_create_text("101", 106, 57.5, height=2.0, layer="INSTRUMENT-TAG-TEXT") - -# control valve symbol = bowtie (2 triangles) — built from lines for now -fcv_top1 = await entity_create_line(150, 60, 154, 64, layer="PROCESS-VALVES") -fcv_top2 = await entity_create_line(150, 60, 154, 56, layer="PROCESS-VALVES") -fcv_bot1 = await entity_create_line(154, 64, 158, 60, layer="PROCESS-VALVES") -fcv_bot2 = await entity_create_line(154, 56, 158, 60, layer="PROCESS-VALVES") -await entity_create_text("FCV-101", 145, 70, height=2.5, layer="INSTRUMENT-TAG-TEXT") - -# 6. SIGNAL LINES — FT bubble up to controller, then to FCV (dashed) -sig1 = await entity_create_line(110, 64, 110, 90, layer="INSTRUMENT-LINE-SIGNAL") -controller = await entity_create_circle(110, 95, 5, layer="INSTRUMENT-SYMBOL") -await entity_create_text("FIC", 107, 95.5, height=2.0, layer="INSTRUMENT-TAG-TEXT") -await entity_create_text("101", 106, 92.5, height=2.0, layer="INSTRUMENT-TAG-TEXT") -sig2 = await entity_create_line(110, 90, 154, 90, layer="INSTRUMENT-LINE-SIGNAL") -sig3 = await entity_create_line(154, 90, 154, 64, layer="INSTRUMENT-LINE-SIGNAL") - -# 7. (no dimensions — P&ID is schematic, not dimensioned) - -# 8. CRITIQUE — focus on what matters for P&ID -issues = await drawing_critique(focus=["layer_color", "iso128", "construction_left"]) -assert issues == [], f"Premium gate failed: {issues}" - -# 9. FINALIZE -await titleblock_apply_iso_a3( - title="P&ID — Flow Control Loop 101", - drawing_number="ANK-PID-2026-001", - revision="0", - scale="NTS", - drawn_by="UE", - company="Anka-Makine", -) - -await drawing_finalize( - save_path=r"C:\work\anka\pid-loop-101.dwg", - screenshot_path=r"C:\work\anka\pid-loop-101.png", -) -``` - -## What this example demonstrates - -- **`layer_set_id="pid"`** in `drawing_plan` triggers the P&ID layer bootstrap - (PROCESS-PIPING-MAIN, INSTRUMENT-SYMBOL, INSTRUMENT-LINE-SIGNAL, etc). -- **Layer-by-purpose**: process lines on PROCESS-PIPING-MAIN (heavy 0.7 mm), - signal lines on INSTRUMENT-LINE-SIGNAL (dashed 0.18), tags on - INSTRUMENT-TAG-TEXT. -- **ISA-5.1 tags** in text: `FT-101`, `FCV-101`, `FIC-101`, `P-101`, `V-201`. -- **No dimensions** — P&IDs are schematic. We pass a focused critique that - doesn't run dimension overlap checks. -- The same `drawing_finalize` infrastructure that handles mechanical drawings - works for P&IDs without modification. - -## Where to grow - -V2/V3 features that would polish this example: -- Symbol library blocks for pumps, valves, instruments (`block_insert`). -- ISO 14617 graphic-symbol catalog as MCP tools. -- Equipment-tag and line-tag auto-numbering. diff --git a/.claude/skills/autocad-mcp-premium/examples/premium-shaft.md b/.claude/skills/autocad-mcp-premium/examples/premium-shaft.md deleted file mode 100644 index ba8d369..0000000 --- a/.claude/skills/autocad-mcp-premium/examples/premium-shaft.md +++ /dev/null @@ -1,125 +0,0 @@ -# Example — Premium Shaft Drawing - -A fully worked example: power-transmission shaft Ø30 × 200 mm with two keyway -seats (10 × 4 × 50 mm each per DIN 6885), drawn at 1:2 on A4. - -## Goal - -```text - 50 100 50 - ┌──┴──┬───┴───┬──┴──┐ - │ │ │ │ keyway both ends, M0.04 surface finish - └─────┴───────┴─────┘ bearing seat in middle (Ø30 h7) - ────── 200 ────── -``` - -## Full workflow - -```python -# 1. PLAN — commit intent first -plan = await drawing_plan( - intent="Power transmission shaft Ø30 x 200, 2× DIN 6885 keyway", - sheet_size="A4", - scale=0.5, # 1:2 (drawing is half real size) - layer_set_id="mech", - dim_style="chain", - notes=[ - "Material: C45 normalized", - "Surface finish Ra 1.6 µm overall", - "Keyways: 10 x 4 x 50 mm per DIN 6885 A", - ], -) - -# 2. LAYERS — mech standard -await drawing_apply_iso_layers("mech") - -# 3. CONSTRUCTION — centerline as reference -cl = await construction_xline(0, 0, 0) # horizontal centerline at y=0 - -# 4. GEOMETRY — front view (top half mirrored later) -top_edge = await entity_create_line(0, 15, 200, 15, layer="GEOMETRY") -right_edge = await entity_create_line(200, 15, 200, -15, layer="GEOMETRY") -bot_edge = await entity_create_line(200, -15, 0, -15, layer="GEOMETRY") -left_edge = await entity_create_line(0, -15, 0, 15, layer="GEOMETRY") - -# 4b. KEYWAY LEFT — use the engineering primitive (per CLAUDE.md rule) -keyway_l = await keyway_draw_section( - bore_x=25, bore_y=0, bore_diameter=30, - keyway_width=10, keyway_depth=4, -) - -# 4c. KEYWAY RIGHT — symmetric on the other end -keyway_r = await keyway_draw_section( - bore_x=175, bore_y=0, bore_diameter=30, - keyway_width=10, keyway_depth=4, -) - -# 4d. Centerline (engineering layer, dashed) -center = await entity_create_line(-10, 0, 210, 0, layer="CENTER") - -# 5. CORNERS — small chamfer at each end (1×45°) -await entity_chamfer(top_edge["handle"], left_edge["handle"], dist1=1) -await entity_chamfer(top_edge["handle"], right_edge["handle"], dist1=1) -await entity_chamfer(bot_edge["handle"], left_edge["handle"], dist1=1) -await entity_chamfer(bot_edge["handle"], right_edge["handle"], dist1=1) - -# 6. SELECT geometry for dim -geometry = await entity_select_smart({ - "type": "LINE", - "layer": "GEOMETRY", - "length_range": [40, 220], # exclude small chamfer lines -}) - -# 7. DIMENSIONS — chain dimensioning, 12 mm offset above the part -dims = await dimension_auto( - [e["handle"] for e in geometry], - style="chain", - offset=12.0, -) - -# 8. CRITIQUE — must be empty before finalize -issues = await drawing_critique(focus=None) -if issues: - for i in issues: - print(f" [{i['severity']}] {i['focus']}: {i['message']}") - raise RuntimeError("Premium gate failed; fix before finalize") - -# 9. CLEAR + TITLEBLOCK + FINALIZE -await construction_clear() - -await titleblock_apply_iso_a3( - title="Transmission Shaft", - drawing_number="ANK-SH-2026-001", - revision="A", - scale="1:2", - material="C45 normalized", - drawn_by="UE", - company="Anka-Makine", -) - -result = await drawing_finalize( - save_path=r"C:\work\anka\sh-2026-001.dwg", - screenshot_path=r"C:\work\anka\sh-2026-001.png", - expected={ - "part_type": "shaft", - "must_have_keyway": True, - "must_have_bore": False, # solid shaft, not bored - }, -) -``` - -## What this example demonstrates - -- **Plan-first workflow** (rule 1) -- **Engineering primitives** for the keyway, not hand-drawn (rule 8 + CLAUDE.md - production drawing rule) -- **Construction line** for the centerline reference, cleared before finalize -- **Corner explicitness** via `entity_chamfer` (rule 5) -- **`entity_select_smart`** with `length_range` to filter out the tiny chamfer - lines (rule 8 — meta-tools over loops) -- **`dimension_auto`** with chain style (rule 6) -- **Critique gate** before finalize (rule 7) -- **`titleblock_apply_iso_a3`** + `drawing_finalize` with `expected` contract - -The whole drawing comes out reproducible: the same call sequence generates an -identical DXF every time, suitable for `git diff` and ISO 9001 traceability. diff --git a/.claude/skills/autocad-mcp-premium/references/corner-ops.md b/.claude/skills/autocad-mcp-premium/references/corner-ops.md deleted file mode 100644 index 2611a80..0000000 --- a/.claude/skills/autocad-mcp-premium/references/corner-ops.md +++ /dev/null @@ -1,77 +0,0 @@ -# Corner Operations Reference - -V1 supports **LINE+LINE** only. Other entity combinations (line+arc, line+circle, -polyline-segment) raise `RuntimeError` and will land in V2/V3. - -## Decision tree - -```text - ┌────── two lines should meet at a sharp corner - │ → entity_trim (when one line overshoots) - │ → entity_extend (when one line falls short) - │ - Need to close ─┼────── two lines should meet at a rounded corner (R>0) - a corner? │ → entity_fillet - │ - └────── two lines should meet at an angled bevel - → entity_chamfer -``` - -## `entity_trim(target_handle, cutter_handle, keep_x, keep_y)` - -- `target` — the line being shortened. -- `cutter` — treated as an **infinite ray** (AutoCAD's "implied extend" trim). -- `keep_x/keep_y` — a point on the side of `target` you want to KEEP. -- Returns the (in-place modified) target's `EntityInfo`. - -**Tip:** use `point_from_snap(target_handle, "mid")` or another snap to compute -`keep_x/keep_y` deterministically. - -## `entity_extend(target_handle, boundary_handle, end_x=None, end_y=None)` - -- Extends `target` until it hits `boundary` (treated as infinite). -- `end_x/end_y=None` → auto-pick the target endpoint nearest the boundary midpoint. -- Pass `end_x/end_y` explicitly to disambiguate when you need to extend a - specific endpoint. - -## `entity_fillet(handle1, handle2, radius, trim=True)` - -- Inserts a tangent **ARC** between the two lines. -- `radius=0` → corner-merge (no arc, only trim). -- `trim=False` → leave source lines untouched (rare; AutoCAD default is True). -- Returns the new ARC handle (or the first source line for `radius=0`). - -## `entity_chamfer(handle1, handle2, dist1, dist2=None, trim=True)` - -- Inserts a straight chamfer **LINE** between the two source lines. -- `dist2=None` → symmetric (`dist2 = dist1`); supply for asymmetric chamfers. -- Returns the new chamfer LINE handle. - -## Corner-case table - -| Vaka | Behaviour | -|------|-----------| -| Lines are parallel / anti-parallel | `RuntimeError("…parallel…")` | -| Same handle for both | `RuntimeError("…same…")` | -| One handle is not a LINE | `RuntimeError("…LINE+LINE only…")` | -| Intersection beyond segment ends | trim/fillet still work (implied extend) | -| Negative radius / zero distance | `RuntimeError` | -| Tiny gap (< 0.5 mm) between lines | flagged by `drawing_critique` as `untrimmed_corner` | - -## Worked example — L-bracket inner corner R5 - -```python -l1 = await entity_create_line(0, 0, 100, 0) # bottom edge -l2 = await entity_create_line(100, 0, 100, 80) # right edge -arc = await entity_fillet(l1.handle, l2.handle, radius=5) -# arc.center == (95, 5); arc.radius == 5 -# l1.end is now (95, 0); l2.start is now (100, 5) -``` - -## Why this matters - -LLMs without these tools compute corner geometry by hand: they pick endpoints, -solve intersections, and write coordinates. That fails the moment a line is -slightly off, an angle is non-orthogonal, or the user changes a length. The -corner ops + `point_from_snap` move that math out of the LLM's head and into -deterministic library code. diff --git a/.claude/skills/autocad-mcp-premium/references/iso-standards.md b/.claude/skills/autocad-mcp-premium/references/iso-standards.md deleted file mode 100644 index acb585d..0000000 --- a/.claude/skills/autocad-mcp-premium/references/iso-standards.md +++ /dev/null @@ -1,109 +0,0 @@ -# ISO Standards Reference - -Distilled tables for the standards this skill enforces. - -## ISO 128 — Line widths - -Allowed lineweight set (mm), with 4:2:1 ratio rule: - -| Width | Common use | -|-------|------------| -| 0.13 | Hatch, dim text shadow | -| 0.18 | Hidden, center, phantom, dim text | -| 0.25 | Default text, leader | -| 0.35 | Auxiliary visible (where contrast needed) | -| 0.50 | Visible solid edges (most common production weight) | -| 0.70 | Heavy solid (large-scale parts), P&ID main process | -| 1.00 | Border, key heavy outlines | -| 1.40 | Title borders | -| 2.00 | Sheet outer border | - -**Rule:** every layer's lineweight must be one of these (or 0.0 = "Default" = -inherits from active style). Anything else fails `drawing_critique` focus -`iso128`. - -## ISO 128 — Line types - -| Type | Linetype name | Used for | -|------|---------------|----------| -| Continuous thick | `Continuous` (0.50+ mm) | Visible edges | -| Dashed thin | `HIDDEN` (0.18–0.25) | Hidden edges | -| Chain thin | `CENTER` (0.18) | Centerlines, axes of symmetry | -| Chain double-dashed | `PHANTOM` (0.18) | Section / cutting plane / alternate position | -| Continuous thin | `Continuous` (0.18–0.25) | Dimensions, hatch, leaders | - -## ISO 13567 — Layer naming (4-field) - -Format: `---` - -| Field | Length | Examples | -|-------|--------|----------| -| Agent | 2 chars | A=Architectural, S=Structural, M=Mechanical, P=Plumbing, E=Electrical | -| Element | 6 chars | GEOMET=geometry, HIDDEN=hidden edges, DIMEN=dimensions, TITLE=title block | -| Presentation | 2 chars | E=edge, T=text, H=hatch | -| Status | 1 char | N=new, E=existing, D=demolish | - -Example layer set ships in `engineering/layers.py::ISO13567_LAYERS`. - -## ISO 129-1 — Dimensioning rules - -1. **Each dimension is given once** — never repeat. -2. **Extension lines** project at least 8× line width past the dim line. -3. **Decimal separator** is the comma (per ISO; we accept point too). -4. **Text orientation**: read from below the drawing or from the right side. -5. **Font**: ISO 3098 (`isocp.shx` ships with AutoCAD). -6. **Dimension styles**: chain, baseline, ordinate are the three standard - layouts — choose one per drawing and stick to it (`dim_style` in PlanSpec). - -## ISO 7200 — Title-block fields - -Mandatory: -- Legal owner (Anka-Makine) -- Drawing number -- Title -- Date -- Sheet / total sheets -- Approver name + signature -- Creator name -- Document type - -Common optional: -- Revision -- Scale (matches PlanSpec.scale) -- Projection symbol (1st angle ISO E vs 3rd angle ISO A) - -This skill assumes you call `titleblock_apply_iso_a3` (or its A3 cousin) to -populate these fields after `drawing_finalize`'s validator runs. - -## ISO 14617 / 10628 — P&ID symbology - -When `layer_set_id="pid"` is passed to `drawing_plan`, the bootstrap creates -the P&ID layer set: - -| Layer | Linetype | Lineweight | Purpose | -|-------|----------|------------|---------| -| `PROCESS-PIPING-MAIN` | Continuous | 0.70 | Main process line (heavy) | -| `PROCESS-PIPING-SECONDARY` | Continuous | 0.50 | Secondary process line | -| `PROCESS-EQUIPMENT` | Continuous | 0.50 | Vessels / pumps / drums | -| `PROCESS-VALVES` | Continuous | 0.35 | Valves & fittings | -| `INSTRUMENT-SYMBOL` | Continuous | 0.35 | Instrument bubbles (FCFs) | -| `INSTRUMENT-LINE-SIGNAL` | DASHED | 0.18 | Pneumatic / electrical signal | -| `INSTRUMENT-TAG-TEXT` | Continuous | 0.25 | Tag labels | -| `ELECTRICAL-LINE` | DASHDOT | 0.18 | Electrical line | -| `UTILITY-LINE` | PHANTOM | 0.25 | Steam / water / air utility | -| `INSULATION-HATCH` | Continuous | 0.13 | Insulation hatching | - -### ISA-5.1 tag conventions (P&ID instruments) - -`-` - -| First letter (measured variable) | Second letter (function) | -|----------------------------------|--------------------------| -| F = Flow | T = Transmitter | -| P = Pressure | I = Indicator | -| T = Temperature | C = Controller | -| L = Level | V = Valve | -| A = Analytical | S = Switch | - -Examples: `FT-101` (flow transmitter, loop 101), `PCV-203A` (pressure control -valve, loop 203, A spare). diff --git a/.claude/skills/autocad-mcp-premium/references/meta-planning.md b/.claude/skills/autocad-mcp-premium/references/meta-planning.md deleted file mode 100644 index 4b03d8a..0000000 --- a/.claude/skills/autocad-mcp-premium/references/meta-planning.md +++ /dev/null @@ -1,113 +0,0 @@ -# Meta-Planning Reference - -The premium meta-tool layer wraps the entity primitives in a quality-first -workflow. Schemas live in `engineering/plan_spec.py`; checks live in -`engineering/critique.py`. - -## `drawing_plan(intent, sheet_size, scale, layer_set_id, view_count, dim_style, notes)` - -Commits a `PlanSpec` to the backend instance. Always call FIRST. - -```yaml -PlanSpec: - intent: str # "L-bracket 50x80" - sheet_size: A4 | A3 | A2 | A1 | A0 # default A3 - scale: float # 1.0 = 1:1 - layer_set_id: mech | pid | iso13567 # default mech - view_count: int # default 1 - dim_style: chain | baseline | ordinate | mixed # default chain - notes: list[str] # free-form constraints -``` - -The PlanSpec is held by the backend (not persisted to DXF) and replayed by -`drawing_critique` at finalize time. - -## `drawing_critique(focus=None)` - -Returns a list of `Issue` instances. **Empty list = pass.** - -```yaml -Issue: - severity: error | warning | info - focus: - message: str - handles: list[str] # entities involved - detail: dict # numeric context -``` - -`CritiqueFocus` is a **closed enum** — adding a new focus requires a code change -to `plan_spec.py` AND `critique.py`. This is intentional: it keeps the critique -scope from drifting. - -| Focus | Checks | -|-------|--------| -| `iso128` | Layer lineweights are in {0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0, 1.4, 2.0} mm | -| `layer_color` | Engineering layers keep their canonical ACI color | -| `dim_overlap` | No two dimensions within 5 mm of each other | -| `untrimmed_corner` | No two LINE endpoints within 0.5 mm but not exactly equal | -| `duplicate_entities` | No two LINEs with identical (or reversed) endpoints | -| `construction_left` | CONSTRUCTION layer is empty | - -`focus=None` runs all six. Pass a list (e.g. `["construction_left", "untrimmed_corner"]`) -to scope a check during iterative editing. - -## `point_from_snap(handle, snap, ref_x, ref_y)` - -Eliminates LLM coordinate guessing. Returns `(x, y)` tuple. - -| `snap` | LINE | CIRCLE/ARC | Required | -|--------|------|-----------|----------| -| `end` | start or end (closer to ref) | arc start or end | — (uses ref if given) | -| `mid` | midpoint | arc midpoint | — | -| `center` | — | center | — | -| `quad` | — | nearest of E/N/W/S quadrants | — (uses ref if given) | -| `perp` | foot of perpendicular from ref | — | `ref_x`, `ref_y` | -| `near` | nearest point on segment (clamped) | nearest on circle/arc | `ref_x`, `ref_y` | -| `int` | (V2 — needs 2nd handle) | | | - -## `construction_xline(x, y, angle_deg, layer="CONSTRUCTION")` - -Creates an infinite construction line on a non-printing layer (color 250, -lightest weight). Use as scaffolding when laying out geometry. - -`construction_clear(layer="CONSTRUCTION")` — bulk-deletes everything on the -layer. Returns `{"ok": True, "deleted": N}`. **Idempotent.** - -## `drawing_apply_iso_layers(standard)` - -| `standard` | Layer count | Use case | -|------------|-------------|----------| -| `mech` | 10 | Mechanical part / assembly drawings | -| `pid` | 15 | Process & Instrumentation Diagrams (ISO 10628) | -| `iso13567` | 10 | ISO 13567 layer-naming convention starter | - -Idempotent — existing layers are not modified. - -## `dimension_auto(handles, style, offset)` - -V1 supports LINE entities only. Generates one or more dimensions using the -chosen style. - -| `style` | Behaviour | -|---------|-----------| -| `chain` | One linear dim per handle, perpendicular-offset from each segment | -| `baseline` | All dims share the first segment's start point as baseline | -| `ordinate` | X and Y coordinate dims for each segment endpoint | - -`offset` (mm) controls the dim-line distance from the geometry. - -## `entity_select_smart(predicate)` - -Predicate is a dict; **all keys optional, AND-ed**: - -```yaml -predicate: - type: LINE | CIRCLE | ARC | ... # entity type filter - layer: # exact match - near: [x, y, radius] # entity within radius of point - length_range: [min, max] # LINE/ARC only - color: # exact match -``` - -Returns a list of `EntityInfo`. Use this in lieu of memorising handles when -chaining `dimension_auto` or other operations. diff --git a/.claude/skills/autocad-mcp-premium/references/premium-workflow.md b/.claude/skills/autocad-mcp-premium/references/premium-workflow.md deleted file mode 100644 index e613c7e..0000000 --- a/.claude/skills/autocad-mcp-premium/references/premium-workflow.md +++ /dev/null @@ -1,159 +0,0 @@ -# Premium Workflow — Full Examples - -The 9-step workflow walked through end-to-end. Use these as templates. - -## Template 1 — Mechanical part (L-bracket with rounded inner corner) - -```python -# 1. PLAN -plan = await drawing_plan( - intent="L-bracket 50x80, R5 inner corner, 5mm thick", - sheet_size="A4", - scale=1.0, - layer_set_id="mech", - dim_style="chain", -) - -# 2. LAYERS -await drawing_apply_iso_layers("mech") - -# 3. CONSTRUCTION (optional — set up reference grid) -await construction_xline(0, 0, 0) # horizontal axis -await construction_xline(0, 0, 90) # vertical axis - -# 4. GEOMETRY (on GEOMETRY layer) -l1 = await entity_create_line(0, 0, 100, 0, layer="GEOMETRY") -l2 = await entity_create_line(100, 0, 100, 80, layer="GEOMETRY") - -# 5. CORNERS -arc = await entity_fillet(l1["handle"], l2["handle"], radius=5) - -# 6. SELECT for dimensioning (avoid handle memorization) -geometry_lines = await entity_select_smart({ - "type": "LINE", - "layer": "GEOMETRY", -}) - -# 7. DIMENSIONS -dims = await dimension_auto( - [e["handle"] for e in geometry_lines], - style="chain", - offset=12.0, -) - -# 8. CRITIQUE -issues = await drawing_critique(focus=None) -assert issues == [], f"Premium gate failed: {issues}" - -# 9. CLEAR + FINALIZE -await construction_clear() -await drawing_finalize( - save_path="C:/work/l-bracket.dwg", - screenshot_path="C:/work/l-bracket.png", -) -``` - -## Template 2 — P&ID flow control loop - -```python -# 1. PLAN -plan = await drawing_plan( - intent="Flow control loop 101: pump → valve → vessel", - sheet_size="A3", - scale=1.0, - layer_set_id="pid", - dim_style="ordinate", - notes=["ISA-5.1 tag conventions", "All process lines on PROCESS-PIPING-MAIN"], -) - -# 2. LAYERS — bootstraps PROCESS-PIPING-MAIN, INSTRUMENT-*, etc. -await drawing_apply_iso_layers("pid") - -# 3. EQUIPMENT (vessels, pumps — use blocks ideally) -await entity_create_circle(50, 50, 8, layer="PROCESS-EQUIPMENT") # pump body -await entity_create_rectangle(150, 30, 200, 70, layer="PROCESS-EQUIPMENT") # vessel - -# 4. PROCESS LINES -main = await entity_create_line(58, 50, 150, 50, layer="PROCESS-PIPING-MAIN") - -# 5. INSTRUMENT BUBBLES (circle + tag text) -ft = await entity_create_circle(100, 70, 5, layer="INSTRUMENT-SYMBOL") -await entity_create_text("FT-101", 95, 68, height=2.5, layer="INSTRUMENT-TAG-TEXT") - -# 6. SIGNAL LINE (instrument bubble → controller) -await entity_create_line(100, 75, 100, 90, layer="INSTRUMENT-LINE-SIGNAL") - -# 7. (no dimensions on P&ID typically — skip) - -# 8. CRITIQUE — focus on what matters for P&ID -issues = await drawing_critique(focus=["layer_color", "iso128", "construction_left"]) -assert issues == [] - -# 9. FINALIZE -await drawing_finalize(save_path="C:/work/loop-101.dwg") -``` - -## Template 3 — Iterative drawing (corrections welcome) - -```python -# Initial plan -plan = await drawing_plan("Bracket draft", sheet_size="A4") -await drawing_apply_iso_layers("mech") - -# Draw something wrong: leaves a gap -l1 = await entity_create_line(0, 0, 49.8, 0, layer="GEOMETRY") -l2 = await entity_create_line(50, 0, 50, 30, layer="GEOMETRY") - -# Critique — flags untrimmed_corner -issues = await drawing_critique(focus=["untrimmed_corner"]) -# issues[0].handles = [l1.handle, l2.handle] - -# Fix in place: extend l1 to meet l2 -await entity_extend(l1["handle"], l2["handle"]) - -# Re-critique -issues = await drawing_critique(focus=["untrimmed_corner"]) -assert issues == [] -``` - -## Common patterns - -### Snap-driven coordinates - -```python -# BAD: guessing -await entity_create_line(50, 50, 100, 50) - -# GOOD: snap from existing geometry -mid = await point_from_snap(some_line["handle"], "mid") -end = await point_from_snap(other_line["handle"], "end", ref_x=100, ref_y=50) -await entity_create_line(mid["x"], mid["y"], end["x"], end["y"]) -``` - -### Construction-first workflow - -```python -# Lay out reference geometry -await construction_xline(0, 0, 0) # x-axis -await construction_xline(0, 0, 90) # y-axis -await construction_xline(100, 0, 90) # vertical at x=100 - -# Snap real geometry to construction intersections -ref = await point_from_snap(some_line["handle"], "perp", ref_x=0, ref_y=50) -# ... create real geometry on GEOMETRY layer ... - -# Wipe scaffold before finalize -await construction_clear() -``` - -### Block-first repetition - -```python -# When 12 holes are needed: create one block, then array -hole = await entity_create_circle(0, 0, 5, layer="GEOMETRY") -await block_create_from_entities("HOLE_M10", [hole["handle"]], 0, 0) - -# Insert + array — far cheaper than 12 entity_create_circle calls -ref = await block_insert("HOLE_M10", x=0, y=0) -await entity_array_polar(ref["handle"], count=12, fill_angle=360, center_x=50, center_y=50) -``` diff --git a/.cursor/rules/Master-Rules.mdc b/.cursor/rules/Master-Rules.mdc deleted file mode 100644 index 2cec461..0000000 --- a/.cursor/rules/Master-Rules.mdc +++ /dev/null @@ -1,47 +0,0 @@ ---- -alwaysApply: true ---- - -ASK MODE – Mandatory and Absolute Rules - -1. AskQuestion Requirement -- Every generated response must end with AskQuestion. The conversation can never be terminated. -- AskQuestion is the final action. -- It contains exactly one question and exactly 2 options. -- The options must be clear and closed-ended. -- The “Other” option is visible in the user interface (the user may enter a custom prompt). -- The fallback option must never terminate the conversation. - ---- - -1. FRT Lock (Infinite Continuity) -- Unless the user writes “FRT” alone, Ask Mode must never end. -- Expressions such as “finished”, “stop”, “end”, thanks, or making a selection are invalid. -- The only exit key for Ask Mode is: FRT -- If FRT appears within another sentence, it is invalid. It must be written alone and in uppercase. - ---- - -1. Medium / Large Task Rule -- Sub-agent usage is mandatory. -- Model: Claude 4.6 Max Thinking Fast max (the only authorized model) -- A single sub-agent cannot be used. -- 4 parallel sub-agent capacity must be utilized. - ---- - -1. Sleep Protocol -- sleep 'value' -- Runs in the foreground, no timeout (minimum 70 seconds if required). -- Only run_terminal_cmd or Shell. -- is_background is prohibited. - ---- - -1. Operational Principle -- Priority: Speed + Accuracy -- Bulk changes: search_replace -- Scattered changes: read_file → edit → write -- Line-based single changes are not allowed. -- Parallel execution is essential. -1. Please dont switch ‘’Plan Mode‘‘ diff --git a/.cursor/skills/autocad-mcp/SKILL.md b/.cursor/skills/autocad-mcp/SKILL.md deleted file mode 100644 index 38c8798..0000000 --- a/.cursor/skills/autocad-mcp/SKILL.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -name: autocad-mcp -description: | - Automate AutoCAD drawing operations through AutoCAD MCP Pro server (FastMCP 3.0). - 86 tools for drawings, entities, layers, blocks, dimensions, analysis, batch ops, - templates, and validation. Dual backends: COM (live AutoCAD) and ezdxf (headless DXF). - - Trigger when: (1) User mentions AutoCAD, DXF, DWG, or CAD drawings, - (2) User wants to create/modify engineering drawings programmatically, - (3) Working with tools prefixed drawing_, entity_, layer_, block_, dimension_, - analysis_, view_, system_, template_, or validation_, - (4) User asks about CAD automation, drafting workflows, or MCP server setup. ---- - -# AutoCAD MCP Pro - -## Contents - -- [Quick Start](#quick-start) -- [Backend Selection](#backend-selection) -- [Key Concepts](#key-concepts) -- [Tool Groups](#tool-groups) -- [Common Workflows](#common-workflows) -- [Security Rules](#security-rules) -- [Architecture](#architecture) -- [Troubleshooting](#troubleshooting) -- [Examples](#examples) - -## Quick Start - -Standard workflow for any drawing task: - -``` -1. drawing_new() # or drawing_open(path) -2. layer_create(name, color, linetype) # set up layers -3. entity_create_*(...) # draw geometry -4. dimension_*(...) # add dimensions -5. analysis_entity_stats() # verify drawing -6. drawing_save() # persist changes -``` - -Use `transaction_begin()` before complex operations. If something goes wrong, `transaction_rollback()` reverts all changes since begin. - -For batch operations, use `entity_batch_create(entities)` with a list of entity definitions — handles are returned for each created entity. - -## Backend Selection - -| Environment Variable | Behavior | -|---------------------|----------| -| `AUTOCAD_MCP_BACKEND=auto` | Try COM first, fall back to ezdxf (default) | -| `AUTOCAD_MCP_BACKEND=com` | Force COM — requires running AutoCAD on Windows | -| `AUTOCAD_MCP_BACKEND=ezdxf` | Force ezdxf — headless DXF ops, no AutoCAD needed | - -COM-only features: `system_run_command`, `system_run_lisp`, `view_zoom_extents/window` (view ops are no-ops in ezdxf). Screenshots: COM uses Win32 capture, ezdxf uses matplotlib render. - -## Key Concepts - -**Entity handles**: Hex strings (e.g. `"1A2B"`) returned by all create/copy operations. Required for modify/query operations (`entity_get`, `entity_move`, `entity_delete`, etc.). - -**Coordinates**: Drawing units (mm default). Angles in degrees, counter-clockwise from X axis. - -**ACI colors**: 1–255 = specific colors, 256 = ByLayer, 0 = ByBlock. Common: 1=red, 2=yellow, 3=green, 4=cyan, 5=blue, 6=magenta, 7=white. - -**Transactions**: `transaction_begin` → operations → `transaction_commit` or `transaction_rollback`. ezdxf uses full DXF snapshots; COM uses native undo marks. - -## Tool Groups - -| Group | Count | Key Tools | Details | -|-------|-------|-----------|---------| -| Drawing Management | 11 | `drawing_new`, `drawing_open`, `drawing_save`, `drawing_export_*` | [tools-drawing.md](references/tools-drawing.md) | -| Entity Creation | 13 | `entity_create_line/circle/arc/polyline/text/hatch/...` | [tools-entity-creation.md](references/tools-entity-creation.md) | -| Entity Modification | 13 | `entity_move/copy/rotate/scale/mirror/offset/delete/...` | [tools-entity-modification.md](references/tools-entity-modification.md) | -| Dimensions | 5 | `dimension_linear/aligned/angular/radius/diameter` | [tools-dimensions.md](references/tools-dimensions.md) | -| Layer Management | 12 | `layer_create/delete/freeze/thaw/lock/unlock/isolate/...` | [tools-layers.md](references/tools-layers.md) | -| Block Operations | 7 | `block_insert/explode/create_from_entities/attributes/...` | [tools-blocks.md](references/tools-blocks.md) | -| Analysis | 9 | `analysis_entity_stats/find_in_region/measure_*/bounding_box` | [tools-analysis.md](references/tools-analysis.md) | -| Batch & Templates | 4 | `entity_batch_create/modify`, `template_apply_layers/list` | [tools-batch-template.md](references/tools-batch-template.md) | -| Validation | 1 | `validation_check` | [tools-batch-template.md](references/tools-batch-template.md) | -| View & Screenshot | 5 | `view_zoom_extents/window/screenshot/zoom_and_screenshot` | [tools-view-system.md](references/tools-view-system.md) | -| Transactions | 3 | `transaction_begin/commit/rollback` | [tools-view-system.md](references/tools-view-system.md) | -| System | 6 | `system_status/get_variable/run_command/run_lisp/about` | [tools-view-system.md](references/tools-view-system.md) | - -**Resources** (6): `autocad://drawing`, `autocad://layers`, `autocad://blocks`, `autocad://stats`, `autocad://status`, `autocad://entities/{layer_name}` - -**Prompt templates** (5): `prompt_floor_plan`, `prompt_pid_diagram`, `prompt_electrical_schematic`, `prompt_mechanical_drawing`, `prompt_quick_drawing` - -## Common Workflows - -**New drawing from scratch**: Set up layers first, then draw entities, add dimensions, save. -See [workflows.md](references/workflows.md) for step-by-step checklists. - -**Edit existing DXF**: `drawing_open` → `entity_list` to find entities → modify/delete → save. - -**Batch operations**: `entity_batch_create([{type:"line", ...}, {type:"circle", ...}])` — single call, multiple entities. - -**Template-based**: `template_apply_layers("architectural")` creates standard layer sets, then add content. - -**Quality check**: `validation_check(["empty_layers", "zero_length", "duplicate_entities"])` reports issues. - -## Security Rules - -All file paths pass through `validate_path()`: -- Path traversal blocked (`../` patterns rejected) -- `ALLOWED_PATHS` env restricts accessible directories -- Write operations require `allow_write=True` - -Commands pass through `sanitize_command()`: -- Dangerous patterns blocked: ERASE ALL, DELETE, PURGE, QUIT -- LISP sanitized: `startapp`, `vl-file-delete`, `dos_*` rejected -- Override: `DANGEROUS_COMMANDS_ENABLED=true` - -Details: [security.md](references/security.md) - -## Architecture - -``` -server.py FastMCP 3.0 server (86 tools, 6 resources, 5 prompts) -├── config.py Centralized pydantic-settings configuration -├── security.py Path validation + command sanitization -└── backends/ - ├── base.py AutoCADBackend ABC + data models - ├── ezdxf_backend.py Headless DXF via ezdxf (async via _async wrapper) - └── com_backend.py Live AutoCAD via pywin32 COM (STA single-thread) -``` - -Middleware stack: ErrorHandling → AuditLog → Timing → Logging - -Adding a new tool: (1) abstract method in `base.py` → (2) implement in `ezdxf_backend.py` → (3) implement in `com_backend.py` → (4) register in `server.py` with `@mcp.tool` → (5) test - -Details: [architecture.md](references/architecture.md) - -## Troubleshooting - -| Error | Solution | -|-------|----------| -| "Backend unavailable" | Call `drawing_new()` or `drawing_open(path)` first | -| "Entity handle not found" | Use `entity_list()` to get valid handles | -| "Layer not found" | `layer_list()` to see existing, `layer_create()` to add | -| "Path validation failed" | Use absolute paths within `ALLOWED_PATHS` | -| "Command rejected" | Check `DANGEROUS_COMMANDS_ENABLED` setting | -| "COM backend failed" | Verify AutoCAD is running, or switch to ezdxf | -| "View ops are no-ops" | View/zoom only work with COM backend | - -Full list: [troubleshooting.md](references/troubleshooting.md) - -## Examples - -- [Basic drawing workflow](examples/basic-workflow.md) — Room with walls, door, dimensions -- [Floor plan](examples/floor-plan.md) — Multi-room apartment layout -- [Mechanical part](examples/mechanical-part.md) — Flange with bolt pattern - -## Utility Scripts - -**Setup check**: `python scripts/check_setup.py` — Verify dependencies and configuration - -**Drawing validation**: `python scripts/validate_drawing.py` — Check for common drawing issues diff --git a/.cursor/skills/autocad-mcp/assets/layer-standards.json b/.cursor/skills/autocad-mcp/assets/layer-standards.json deleted file mode 100644 index 0aa2b40..0000000 --- a/.cursor/skills/autocad-mcp/assets/layer-standards.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "architectural": { - "description": "Standard architectural drawing layers", - "layers": [ - {"name": "WALLS", "color": 7, "linetype": "Continuous", "description": "Wall outlines"}, - {"name": "DOORS", "color": 3, "linetype": "Continuous", "description": "Door swings and frames"}, - {"name": "WINDOWS", "color": 5, "linetype": "Continuous", "description": "Window openings"}, - {"name": "DIMENSIONS", "color": 1, "linetype": "Continuous", "description": "Dimension annotations"}, - {"name": "TEXT", "color": 2, "linetype": "Continuous", "description": "Text labels and notes"}, - {"name": "FURNITURE", "color": 8, "linetype": "Continuous", "description": "Furniture outlines"}, - {"name": "PLUMBING", "color": 4, "linetype": "Continuous", "description": "Plumbing fixtures"}, - {"name": "HVAC", "color": 6, "linetype": "Continuous", "description": "HVAC equipment and ducts"} - ] - }, - "mechanical": { - "description": "Standard mechanical drawing layers", - "layers": [ - {"name": "OUTLINE", "color": 7, "linetype": "Continuous", "description": "Part outlines"}, - {"name": "CENTERLINE", "color": 1, "linetype": "CENTER", "description": "Center lines and axes"}, - {"name": "HIDDEN", "color": 8, "linetype": "HIDDEN", "description": "Hidden edges"}, - {"name": "DIMENSIONS", "color": 5, "linetype": "Continuous", "description": "Dimensions"}, - {"name": "NOTES", "color": 2, "linetype": "Continuous", "description": "Notes and callouts"}, - {"name": "SECTION", "color": 3, "linetype": "Continuous", "description": "Section lines and hatching"}, - {"name": "DETAIL", "color": 4, "linetype": "Continuous", "description": "Detail views"} - ] - }, - "electrical": { - "description": "Standard electrical drawing layers", - "layers": [ - {"name": "POWER", "color": 1, "linetype": "Continuous", "description": "Power circuits"}, - {"name": "LIGHTING", "color": 2, "linetype": "Continuous", "description": "Lighting circuits"}, - {"name": "DATA", "color": 3, "linetype": "Continuous", "description": "Data and communication"}, - {"name": "FIRE_ALARM", "color": 4, "linetype": "Continuous", "description": "Fire alarm system"}, - {"name": "HVAC_CTRL", "color": 5, "linetype": "Continuous", "description": "HVAC controls"}, - {"name": "PANELS", "color": 7, "linetype": "Continuous", "description": "Panel boards and equipment"}, - {"name": "CONDUIT", "color": 8, "linetype": "Continuous", "description": "Conduit runs"} - ] - }, - "piping": { - "description": "Standard piping/P&ID drawing layers", - "layers": [ - {"name": "PROCESS", "color": 7, "linetype": "Continuous", "description": "Process piping"}, - {"name": "UTILITY", "color": 3, "linetype": "Continuous", "description": "Utility piping"}, - {"name": "INSTRUMENTS", "color": 1, "linetype": "Continuous", "description": "Instrumentation"}, - {"name": "EQUIPMENT", "color": 5, "linetype": "Continuous", "description": "Equipment outlines"}, - {"name": "VALVES", "color": 2, "linetype": "Continuous", "description": "Valves and fittings"}, - {"name": "LABELS", "color": 4, "linetype": "Continuous", "description": "Labels and tags"} - ] - } -} diff --git a/.cursor/skills/autocad-mcp/examples/basic-workflow.md b/.cursor/skills/autocad-mcp/examples/basic-workflow.md deleted file mode 100644 index 11b9e2c..0000000 --- a/.cursor/skills/autocad-mcp/examples/basic-workflow.md +++ /dev/null @@ -1,72 +0,0 @@ -# Example: Basic Room Drawing - -Create a simple room with walls, a door opening, dimensions, and labels. - -## Setup - -``` -drawing_new() -layer_create("WALLS", color=7) -layer_create("DOORS", color=3) -layer_create("DIMENSIONS", color=1) -layer_create("TEXT", color=2) -``` - -## Draw Walls (5m x 4m room) - -``` -layer_set_current("WALLS") - -# Outer walls as rectangle -entity_create_rectangle(x1=0, y1=0, x2=5000, y2=4000) -# Returns: {handle: "A1", type: "LWPOLYLINE", ...} -``` - -## Add Door Opening (0.9m wide on south wall) - -``` -layer_set_current("DOORS") - -# Door opening represented as an arc (90° swing) -entity_create_arc(cx=2000, cy=0, radius=900, start_angle=0, end_angle=90) -# Returns: {handle: "B2", type: "ARC", ...} - -# Door frame lines -entity_create_line(x1=2000, y1=0, x2=2000, y2=100) -entity_create_line(x1=2900, y1=0, x2=2900, y2=100) -``` - -## Add Dimensions - -``` -layer_set_current("DIMENSIONS") - -# Room width -dimension_linear(x1=0, y1=0, x2=5000, y2=0, dim_x=2500, dim_y=-500) - -# Room height -dimension_linear(x1=5000, y1=0, x2=5000, y2=4000, dim_x=5500, dim_y=2000) - -# Door width -dimension_linear(x1=2000, y1=0, x2=2900, y2=0, dim_x=2450, dim_y=-300) -``` - -## Add Labels - -``` -layer_set_current("TEXT") -entity_create_text(text="BEDROOM", x=2500, y=2000, height=200) -entity_create_text(text="5000", x=2500, y=-800, height=150) -``` - -## Verify and Save - -``` -analysis_entity_stats() -# Expected: ~8 entities across 4 layers - -validation_check(["empty_layers"]) -# Should report no issues - -drawing_save("C:/drawings/room.dxf") -``` diff --git a/.cursor/skills/autocad-mcp/examples/floor-plan.md b/.cursor/skills/autocad-mcp/examples/floor-plan.md deleted file mode 100644 index 6f23c0e..0000000 --- a/.cursor/skills/autocad-mcp/examples/floor-plan.md +++ /dev/null @@ -1,100 +0,0 @@ -# Example: Apartment Floor Plan - -Create a 3-room apartment: living room, bedroom, kitchen with doors and windows. - -## Setup - -``` -drawing_new() -template_apply_layers("architectural") -# Creates: WALLS, DOORS, WINDOWS, DIMENSIONS, TEXT, FURNITURE, PLUMBING, HVAC -``` - -## Draw Exterior Walls (12m x 8m) - -``` -layer_set_current("WALLS") - -# Exterior boundary -entity_create_rectangle(x1=0, y1=0, x2=12000, y2=8000) -``` - -## Draw Interior Walls - -``` -# Living/bedroom divider (vertical at x=5000) -entity_create_line(x1=5000, y1=0, x2=5000, y2=8000) - -# Kitchen divider (horizontal at y=4000, from x=5000) -entity_create_line(x1=5000, y1=4000, x2=12000, y2=4000) -``` - -## Add Doors (0.9m openings) - -``` -layer_set_current("DOORS") - -# Living room door (south wall) -entity_create_arc(cx=2000, cy=0, radius=900, start_angle=0, end_angle=90) - -# Bedroom door (on divider wall) -entity_create_arc(cx=5000, cy=2000, radius=900, start_angle=90, end_angle=180) - -# Kitchen door (on divider wall) -entity_create_arc(cx=7000, cy=4000, radius=900, start_angle=0, end_angle=90) -``` - -## Add Windows (1.5m wide) - -``` -layer_set_current("WINDOWS") - -# Living room window (north wall) -entity_create_line(x1=1500, y1=8000, x2=3000, y2=8000, color=5) - -# Bedroom window (east wall) -entity_create_line(x1=12000, y1=5500, x2=12000, y2=7000, color=5) - -# Kitchen window (east wall) -entity_create_line(x1=12000, y1=1500, x2=12000, y2=3000, color=5) -``` - -## Add Dimensions - -``` -layer_set_current("DIMENSIONS") - -# Overall width -dimension_linear(x1=0, y1=0, x2=12000, y2=0, dim_x=6000, dim_y=-800) - -# Overall height -dimension_linear(x1=0, y1=0, x2=0, y2=8000, dim_x=-800, dim_y=4000) - -# Room widths -dimension_linear(x1=0, y1=8000, x2=5000, y2=8000, dim_x=2500, dim_y=8500) -dimension_linear(x1=5000, y1=8000, x2=12000, y2=8000, dim_x=8500, dim_y=8500) -``` - -## Add Room Labels - -``` -layer_set_current("TEXT") - -entity_create_text(text="LIVING ROOM", x=2500, y=4000, height=300) -entity_create_mtext(text="12.0 m²", x=2500, y=3500, width=2000, height=200) - -entity_create_text(text="BEDROOM", x=8500, y=6000, height=300) -entity_create_mtext(text="28.0 m²", x=8500, y=5500, width=2000, height=200) - -entity_create_text(text="KITCHEN", x=8500, y=2000, height=300) -entity_create_mtext(text="28.0 m²", x=8500, y=1500, width=2000, height=200) -``` - -## Verify - -``` -analysis_entity_stats() -analysis_layer_stats() -validation_check(["empty_layers", "zero_length"]) -drawing_save("C:/drawings/apartment.dxf") -``` diff --git a/.cursor/skills/autocad-mcp/examples/mechanical-part.md b/.cursor/skills/autocad-mcp/examples/mechanical-part.md deleted file mode 100644 index d2a0120..0000000 --- a/.cursor/skills/autocad-mcp/examples/mechanical-part.md +++ /dev/null @@ -1,105 +0,0 @@ -# Example: Flange with Bolt Pattern - -Create a circular flange plate with bolt holes using polar array. - -## Setup - -``` -drawing_new() -template_apply_layers("mechanical") -# Creates: OUTLINE, CENTERLINE, HIDDEN, DIMENSIONS, NOTES, SECTION, DETAIL -``` - -## Draw Flange Outline - -``` -layer_set_current("OUTLINE") - -# Outer flange circle (diameter 200mm) -entity_create_circle(cx=0, cy=0, radius=100) -# Returns: {handle: "F1", ...} - -# Inner bore (diameter 50mm) -entity_create_circle(cx=0, cy=0, radius=25) -# Returns: {handle: "F2", ...} -``` - -## Add Centerlines - -``` -layer_set_current("CENTERLINE") - -# Horizontal centerline -entity_create_line(x1=-120, y1=0, x2=120, y2=0, linetype="CENTER") - -# Vertical centerline -entity_create_line(x1=0, y1=-120, x2=0, y2=120, linetype="CENTER") -``` - -## Create Bolt Hole Pattern - -``` -layer_set_current("OUTLINE") - -# First bolt hole on bolt circle (PCD = 150mm diameter, so radius = 75mm) -entity_create_circle(cx=75, cy=0, radius=6) -# Returns: {handle: "B1", ...} - -# Create polar array: 6 holes evenly spaced -entity_array_polar(handle="B1", center_x=0, center_y=0, count=6, angle=360) -# Returns: list of 5 new handles (original + 5 copies = 6 total) -``` - -## Add Dimensions - -``` -layer_set_current("DIMENSIONS") - -# Outer diameter -dimension_diameter(x1=-100, y1=0, x2=100, y2=0) - -# Inner bore diameter -dimension_diameter(x1=-25, y1=0, x2=25, y2=0) - -# Bolt circle diameter -dimension_radius(center_x=0, center_y=0, radius=75, angle=45) - -# Bolt hole diameter -dimension_diameter(x1=69, y1=0, x2=81, y2=0) -``` - -## Add Notes - -``` -layer_set_current("NOTES") - -entity_create_mtext( - text="FLANGE PLATE\P6x M12 HOLES ON PCD 150\PMATERIAL: SS304", - x=-120, y=-140, - width=240, height=5 -) -``` - -## Create Block from Bolt Hole - -``` -# Select all bolt hole entities and create a reusable block -# (Get handles from the array result) -block_create_from_entities( - name="BOLT_PATTERN_6x150", - handles=["B1", ...], # all bolt hole handles - base_x=0, base_y=0 -) -``` - -## Verify - -``` -analysis_entity_stats() -# Expected: circles, lines, dimensions, mtext - -analysis_bounding_box() -# Expected: approximately -120 to 120 in both axes - -drawing_save("C:/drawings/flange.dxf") -``` diff --git a/.cursor/skills/autocad-mcp/references/architecture.md b/.cursor/skills/autocad-mcp/references/architecture.md deleted file mode 100644 index 6b1dc9f..0000000 --- a/.cursor/skills/autocad-mcp/references/architecture.md +++ /dev/null @@ -1,60 +0,0 @@ -# Architecture - -## File Structure - -``` -server.py FastMCP 3.0 server — all tool/resource/prompt registrations -├── config.py Settings class (pydantic-settings, env vars) -├── security.py validate_path() + sanitize_command() + sanitize_lisp() -└── backends/ - ├── base.py AutoCADBackend ABC + data models - ├── ezdxf_backend.py Headless DXF ops via ezdxf library - └── com_backend.py Live AutoCAD control via pywin32 COM -``` - -## Dual-Backend Strategy Pattern - -`AutoCADBackend` (in `base.py`) is an abstract base class. Both backends implement the full interface. The server creates exactly one backend at startup via `_make_backend()`: - -1. Check `AUTOCAD_MCP_BACKEND` env var -2. If `auto` or `com` on Windows: try COM → fall back to ezdxf -3. If non-Windows or `ezdxf`: use ezdxf - -## Data Models - -Defined in `base.py`: - -- **EntityInfo**: `{handle, type, layer, color, linetype, visible, properties}` -- **LayerInfo**: `{name, color, linetype, lineweight, is_on, is_frozen, is_locked, is_current}` -- **BlockInfo**: `{name, origin, attribute_count, entity_count, is_xref, description}` -- **DrawingInfo**: `{name, full_path, saved, entity_count, layer_count, block_count, extents_min, extents_max, units, version, backend}` -- **CommandResult**: `{ok, payload, error}` - -## Backend Differences - -| Feature | ezdxf | COM | -|---------|-------|-----| -| Platform | Any | Windows only | -| AutoCAD needed | No | Yes (running) | -| View/zoom | No-op | Full control | -| Screenshots | matplotlib render | Win32 window capture | -| Commands/LISP | Not supported | Full support | -| Undo | DXF snapshot stack | Native AutoCAD undo | -| Threading | `asyncio.to_thread` via `_async()` | `ThreadPoolExecutor` (single thread, STA) | -| File formats | DXF only | DWG + DXF | - -## FastMCP Server - -The `mcp` object is configured with: -- **Lifespan**: `autocad_lifespan` — initializes backend, stores in `ctx.lifespan_context["backend"]` -- **Middleware**: ErrorHandling → AuditLog (custom timing) → Timing → Logging -- **`_backend(ctx)`**: Helper that retrieves backend from context, raises `ToolError` if unavailable - -## Adding a New Tool - -1. Add abstract method to `AutoCADBackend` in `backends/base.py` -2. Implement in `EzdxfBackend` — wrap sync calls with `_async(func)` -3. Implement in `ComBackend` — wrap COM calls with `self._com(func)` -4. Register in `server.py`: `@mcp.tool(...)` calling `_backend(ctx).method(...)` -5. Use `_dc(result)` to convert dataclass returns to dicts -6. Write tests in `tests/` diff --git a/.cursor/skills/autocad-mcp/references/security.md b/.cursor/skills/autocad-mcp/references/security.md deleted file mode 100644 index 175e881..0000000 --- a/.cursor/skills/autocad-mcp/references/security.md +++ /dev/null @@ -1,53 +0,0 @@ -# Security - -## Path Validation - -All file-path tools pass through `validate_path()` in `security.py`. - -**How it works**: -- Resolves path via `Path.resolve()` (removes `..`, symlinks) -- Checks against `ALLOWED_PATHS` env var (comma-separated directories) -- If `ALLOWED_PATHS` is empty → all paths allowed (backward compat) -- Separate `allow_write` flag for read vs write operations - -**Tools with path validation**: -- `drawing_open` (read) -- `drawing_save`, `drawing_save_as` (write) -- `drawing_export_dxf`, `drawing_export_pdf` (write) -- `drawing_new` with template (read) - -**Blocked examples**: -- `../../../etc/passwd` → rejected (traversal) -- `C:\Windows\System32\...` → rejected if not in ALLOWED_PATHS -- Relative paths with `..` → always rejected - -## Command Sanitization - -`sanitize_command()` checks `system_run_command` input: - -**Blocked patterns**: `ERASE`, `DELETE`, `PURGE`, `QUIT`, `CLOSE`, `_QUIT`, shell-related commands - -**Override**: Set `DANGEROUS_COMMANDS_ENABLED=true` to bypass. - -## LISP Sanitization - -`sanitize_lisp()` checks `system_run_lisp` input: - -**Blocked functions**: `startapp`, `vl-file-delete`, `vl-file-rename`, `dos_*` (file system access), `command` with dangerous args - -## Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `ALLOWED_PATHS` | (empty) | Comma-separated allowed directories. Empty = all allowed | -| `DANGEROUS_COMMANDS_ENABLED` | `false` | Allow blocked commands | -| `AUTOCAD_MCP_BACKEND` | `auto` | Backend selection | -| `MAX_UNDO_STACK` | `50` | Max undo snapshots (ezdxf) | -| `LOG_LEVEL` | `INFO` | Logging level | - -## Best Practices - -- Always set `ALLOWED_PATHS` in production to restrict file access -- Never pass raw user input to `system_run_command` or `system_run_lisp` -- Use `transaction_begin/rollback` for destructive batch operations -- Review audit logs for suspicious tool calls (AuditMiddleware logs all calls) diff --git a/.cursor/skills/autocad-mcp/references/tools-analysis.md b/.cursor/skills/autocad-mcp/references/tools-analysis.md deleted file mode 100644 index 85e31b3..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-analysis.md +++ /dev/null @@ -1,73 +0,0 @@ -# Analysis Tools (9) - -Tools for querying and measuring drawing contents. - ---- - -## analysis_entity_stats -No parameters. Returns entity counts grouped by type and by layer. - -```json -{ - "total": 150, - "by_type": {"LINE": 50, "CIRCLE": 20, "TEXT": 30, ...}, - "by_layer": {"WALLS": 40, "DOORS": 10, ...} -} -``` - -## analysis_find_in_region -Find entities within a rectangular region. - -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float` | Region minimum corner | -| `x2, y2` | `float` | Region maximum corner | -| `type_filter` | `str \| null` | Filter by entity type | - -Returns list of entity infos within the bounding box. - -## analysis_measure_distance -Measure distance between two points. - -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float` | Point 1 | -| `x2, y2` | `float` | Point 2 | - -Returns `{distance, dx, dy, angle}`. - -## analysis_measure_area -Calculate area of a polygon. - -| Param | Type | Description | -|-------|------|-------------| -| `points` | `list[list[float]]` | Polygon vertices (min 3 points) | - -Returns `{area, perimeter, centroid}`. - -## analysis_bounding_box -No parameters. Returns `{min_x, min_y, max_x, max_y, width, height}` for all entities. - -## analysis_select_by_layer -Get all entities on a specific layer. - -| Param | Type | Description | -|-------|------|-------------| -| `layer_name` | `str` | Layer to select from | - -Returns list of entity infos. - -## analysis_select_by_type -Get all entities of a specific type. - -| Param | Type | Description | -|-------|------|-------------| -| `entity_type` | `str` | Type: LINE, CIRCLE, ARC, LWPOLYLINE, TEXT, MTEXT, INSERT, HATCH, SPLINE, ELLIPSE | - -Returns list of entity infos. - -## analysis_layer_stats -No parameters. Returns per-layer statistics: entity count, types present, color, state. - -## analysis_entities_in_region -Alias/variant of `analysis_find_in_region` — check both for availability. diff --git a/.cursor/skills/autocad-mcp/references/tools-batch-template.md b/.cursor/skills/autocad-mcp/references/tools-batch-template.md deleted file mode 100644 index c21f24a..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-batch-template.md +++ /dev/null @@ -1,83 +0,0 @@ -# Batch, Template & Validation Tools (5) - -## Batch Operations - -### entity_batch_create -Create multiple entities in a single call. Reports progress. - -| Param | Type | Description | -|-------|------|-------------| -| `entities` | `list[dict]` | Entity definitions (see format below) | - -Each dict requires `type` and type-specific params: - -```json -[ - {"type": "line", "x1": 0, "y1": 0, "x2": 100, "y2": 0}, - {"type": "circle", "cx": 50, "cy": 50, "radius": 25}, - {"type": "text", "text": "Hello", "x": 10, "y": 10, "height": 5}, - {"type": "rectangle", "x1": 0, "y1": 0, "x2": 200, "y2": 100}, - {"type": "point", "x": 75, "y": 75}, - {"type": "arc", "cx": 50, "cy": 0, "radius": 10, "start_angle": 0, "end_angle": 180}, - {"type": "polyline", "points": [[0,0],[10,10],[20,0]], "closed": true} -] -``` - -Optional per-entity: `layer`, `color`, `linetype`. - -Returns `{created: [...handles], errors: [...]}`. Partial success is possible. - -### entity_batch_modify -Apply multiple modifications in a single call. Reports progress. - -| Param | Type | Description | -|-------|------|-------------| -| `operations` | `list[dict]` | Operation definitions | - -Each dict requires `handle` and `action`: - -```json -[ - {"handle": "1A", "action": "move", "dx": 10, "dy": 20}, - {"handle": "2B", "action": "rotate", "base_x": 0, "base_y": 0, "angle_deg": 45}, - {"handle": "3C", "action": "scale", "base_x": 0, "base_y": 0, "factor": 2.0}, - {"handle": "4D", "action": "delete"}, - {"handle": "5E", "action": "set_properties", "layer": "WALLS", "color": 1} -] -``` - -Returns results per operation. - ---- - -## Templates - -### template_apply_layers -Apply a standard layer set to the current drawing. - -| Param | Type | Description | -|-------|------|-------------| -| `template` | `str` | Template name: `architectural`, `mechanical`, `electrical`, `piping` | - -Creates predefined layers with standard names, colors, and linetypes. - -### template_list -No parameters. Returns all available templates and their layer definitions. - ---- - -## Validation - -### validation_check -Run quality checks on the drawing. - -| Param | Type | Description | -|-------|------|-------------| -| `checks` | `list[str] \| null` | Check types (default: all) | - -Available checks: -- `empty_layers` — Layers with no entities -- `zero_length` — Lines/arcs with zero length -- `duplicate_entities` — Overlapping identical entities - -Returns `{issues: [{check, severity, message, details}, ...], summary}`. diff --git a/.cursor/skills/autocad-mcp/references/tools-blocks.md b/.cursor/skills/autocad-mcp/references/tools-blocks.md deleted file mode 100644 index 4fe7ace..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-blocks.md +++ /dev/null @@ -1,68 +0,0 @@ -# Block Operations Tools (7) - -Blocks are reusable named groups of entities. A **block definition** is the template; a **block reference** (INSERT entity) is a placed instance with position, scale, and rotation. - ---- - -## block_list -No parameters. Returns all block definitions: `{name, origin, attribute_count, entity_count, is_xref, description}`. - -## block_insert -Place a block reference in the drawing. - -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Block definition name (must exist) | -| `x, y` | `float` | Insertion point | -| `x_scale` | `float` | X scale factor (default: 1.0) | -| `y_scale` | `float` | Y scale factor (default: 1.0) | -| `rotation` | `float` | Rotation in degrees (default: 0) | -| `layer` | `str \| null` | Target layer | - -Returns entity info with handle (type: INSERT). - -## block_explode -Decompose a block reference into individual entities. - -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Block reference (INSERT entity) handle | - -Returns list of new entity handles created from explosion. - -## block_get_attributes -Read attribute values from a block reference. - -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Block reference handle | - -Returns `{tag: value}` dict of all attributes. - -## block_set_attributes -Write attribute values to a block reference. - -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Block reference handle | -| `attributes` | `dict` | `{tag: value}` pairs to set | - -## block_create_from_entities -Create a new block definition from existing entities. - -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | New block definition name | -| `handles` | `list[str]` | Entity handles to include | -| `base_x, base_y` | `float` | Block base/insertion point | - -Entities are moved into the block definition. - -## block_find_references -Find all INSERT entities that reference a specific block. - -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Block definition name | - -Returns list of block reference entity infos. diff --git a/.cursor/skills/autocad-mcp/references/tools-dimensions.md b/.cursor/skills/autocad-mcp/references/tools-dimensions.md deleted file mode 100644 index c00be29..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-dimensions.md +++ /dev/null @@ -1,55 +0,0 @@ -# Dimension Tools (5) - -All dimension tools return entity info with handle. Dimensions are placed on the current layer unless `layer` is specified. - ---- - -## dimension_linear -Linear dimension between two points (horizontal or vertical measurement). - -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float` | First extension line origin | -| `x2, y2` | `float` | Second extension line origin | -| `dim_x, dim_y` | `float` | Dimension line position (controls placement) | -| `layer` | `str \| null` | Target layer | - -## dimension_aligned -Aligned dimension — measures true distance between points regardless of angle. - -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float` | First point | -| `x2, y2` | `float` | Second point | -| `distance` | `float` | Offset distance for dimension line | -| `layer` | `str \| null` | Target layer | - -## dimension_angular -Angular dimension between two lines meeting at a vertex. - -| Param | Type | Description | -|-------|------|-------------| -| `vertex_x, vertex_y` | `float` | Angle vertex point | -| `start_x, start_y` | `float` | First line endpoint | -| `end_x, end_y` | `float` | Second line endpoint | -| `dim_x, dim_y` | `float` | Dimension arc position | -| `layer` | `str \| null` | Target layer | - -## dimension_radius -Radius dimension for circles and arcs. - -| Param | Type | Description | -|-------|------|-------------| -| `center_x, center_y` | `float` | Circle/arc center | -| `radius` | `float` | Radius value | -| `angle` | `float` | Leader angle in degrees (where arrow points) | -| `layer` | `str \| null` | Target layer | - -## dimension_diameter -Diameter dimension across a circle. - -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float` | First point on diameter | -| `x2, y2` | `float` | Opposite point on diameter | -| `layer` | `str \| null` | Target layer | diff --git a/.cursor/skills/autocad-mcp/references/tools-drawing.md b/.cursor/skills/autocad-mcp/references/tools-drawing.md deleted file mode 100644 index 87faa0e..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-drawing.md +++ /dev/null @@ -1,103 +0,0 @@ -# Drawing Management Tools (11) - -## Contents -- [drawing_info](#drawing_info) -- [drawing_new](#drawing_new) -- [drawing_open](#drawing_open) -- [drawing_save](#drawing_save) -- [drawing_save_as](#drawing_save_as) -- [drawing_export_dxf](#drawing_export_dxf) -- [drawing_export_pdf](#drawing_export_pdf) -- [drawing_purge](#drawing_purge) -- [drawing_audit](#drawing_audit) -- [drawing_undo / drawing_redo](#drawing_undo--drawing_redo) - ---- - -## drawing_info -Read-only. Returns comprehensive metadata for the current drawing. - -**Returns**: `{name, full_path, saved, entity_count, layer_count, block_count, extents_min, extents_max, units, version, backend}` - ---- - -## drawing_new -Create a new empty drawing, optionally from a template. - -| Param | Type | Required | Description | -|-------|------|----------|-------------| -| `template` | `str \| null` | No | Path to `.dwt` template file | - -Path validation applied to template. Returns drawing info dict. - ---- - -## drawing_open -Open an existing DWG or DXF file. Reports progress. - -| Param | Type | Required | Description | -|-------|------|----------|-------------| -| `path` | `str` | Yes | Full path to `.dwg` or `.dxf` file | - -Path validation: `allow_write=False`. Returns drawing info dict. - ---- - -## drawing_save -Save current drawing. Uses current path if omitted. - -| Param | Type | Required | Description | -|-------|------|----------|-------------| -| `path` | `str \| null` | No | Optional save path | - -Path validation: `allow_write=True`. - ---- - -## drawing_save_as -Save drawing to a new path. - -| Param | Type | Required | Description | -|-------|------|----------|-------------| -| `path` | `str` | Yes | Full destination path with extension | - -Path validation: `allow_write=True`. - ---- - -## drawing_export_dxf -Export drawing as DXF file. - -| Param | Type | Required | Description | -|-------|------|----------|-------------| -| `path` | `str` | Yes | Output `.dxf` file path | -| `version` | `str` | No | DXF version: `R2010`, `R2013`, `R2018` (default: `R2018`) | - ---- - -## drawing_export_pdf -Export drawing as PDF. Requires `matplotlib`. - -| Param | Type | Required | Description | -|-------|------|----------|-------------| -| `path` | `str` | Yes | Output `.pdf` file path | - ---- - -## drawing_purge -Remove all unused objects (layers, blocks, linetypes, styles). No parameters. - ---- - -## drawing_audit -Run integrity audit — detect and fix drawing errors. No parameters. - ---- - -## drawing_undo / drawing_redo -Undo/redo last operation. - -- **ezdxf**: Snapshot-based (restores full DXF state from `_undo_stack`) -- **COM**: Native AutoCAD undo/redo - -**Note**: For multi-step undo protection, use `transaction_begin/rollback` instead. diff --git a/.cursor/skills/autocad-mcp/references/tools-entity-creation.md b/.cursor/skills/autocad-mcp/references/tools-entity-creation.md deleted file mode 100644 index 4a51767..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-entity-creation.md +++ /dev/null @@ -1,96 +0,0 @@ -# Entity Creation Tools (13) - -All creation tools return `{handle, type, layer, color, linetype, visible, properties}`. The `handle` (hex string) is required for all subsequent operations on that entity. - -Common optional params on all creation tools: - -| Param | Type | Default | Description | -|-------|------|---------|-------------| -| `layer` | `str` | Current layer | Target layer name | -| `color` | `int` | 256 (ByLayer) | ACI color (1-255, 256=ByLayer, 0=ByBlock) | -| `linetype` | `str` | `"ByLayer"` | Linetype name | - ---- - -## entity_create_line -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float` | Start point | -| `x2, y2` | `float` | End point | - -## entity_create_circle -| Param | Type | Description | -|-------|------|-------------| -| `cx, cy` | `float` | Center point | -| `radius` | `float` | Radius | - -## entity_create_arc -| Param | Type | Description | -|-------|------|-------------| -| `cx, cy` | `float` | Center point | -| `radius` | `float` | Radius | -| `start_angle` | `float` | Start angle (degrees, CCW from X) | -| `end_angle` | `float` | End angle (degrees) | - -## entity_create_polyline -| Param | Type | Description | -|-------|------|-------------| -| `points` | `list[list[float]]` | `[[x,y], [x,y], ...]` coordinate pairs | -| `closed` | `bool` | Close the polyline (default: false) | - -## entity_create_rectangle -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float` | First corner | -| `x2, y2` | `float` | Opposite corner | - -Creates an LWPOLYLINE (closed). - -## entity_create_text -| Param | Type | Description | -|-------|------|-------------| -| `text` | `str` | Text content | -| `x, y` | `float` | Insertion point | -| `height` | `float` | Text height (default: 2.5) | -| `rotation` | `float` | Rotation angle in degrees (default: 0) | - -## entity_create_mtext -| Param | Type | Description | -|-------|------|-------------| -| `text` | `str` | Content (`\P` for paragraph break, `{\H...;}` for formatting) | -| `x, y` | `float` | Insertion point | -| `width` | `float` | Text box width (default: 100) | -| `height` | `float` | Character height (default: 2.5) | - -## entity_create_hatch -| Param | Type | Description | -|-------|------|-------------| -| `pattern` | `str` | Pattern name: `SOLID`, `ANSI31`, `ANSI32`, `STEEL`, `GRAVEL`, etc. | -| `boundary_points` | `list[list[float]]` | Closed boundary polygon `[[x,y], ...]` | -| `scale` | `float` | Pattern scale (default: 1.0) | -| `angle` | `float` | Pattern angle (default: 0) | - -## entity_create_spline -| Param | Type | Description | -|-------|------|-------------| -| `fit_points` | `list[list[float]]` | Points the spline passes through | - -## entity_create_ellipse -| Param | Type | Description | -|-------|------|-------------| -| `cx, cy` | `float` | Center point | -| `major_axis_x, major_axis_y` | `float` | Major axis endpoint (relative to center) | -| `ratio` | `float` | Minor/major axis ratio (0.0–1.0) | - -## entity_create_point -| Param | Type | Description | -|-------|------|-------------| -| `x, y` | `float` | Point coordinates | - -## entity_create_block_ref -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Block definition name (must exist) | -| `x, y` | `float` | Insertion point | -| `x_scale, y_scale` | `float` | Scale factors (default: 1.0) | -| `rotation` | `float` | Rotation in degrees (default: 0) | diff --git a/.cursor/skills/autocad-mcp/references/tools-entity-modification.md b/.cursor/skills/autocad-mcp/references/tools-entity-modification.md deleted file mode 100644 index 8ca78bb..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-entity-modification.md +++ /dev/null @@ -1,111 +0,0 @@ -# Entity Modification & Query Tools (13) - -All modify tools require a `handle` (hex string). Get handles from `entity_list()` or any `entity_create_*` return value. - ---- - -## Modification Tools (10) - -### entity_move -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity handle | -| `dx, dy` | `float` | Displacement vector | - -### entity_copy -Returns new entity info with a **new handle**. - -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity to copy | -| `dx, dy` | `float` | Offset for the copy | - -### entity_rotate -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity handle | -| `base_x, base_y` | `float` | Rotation base point | -| `angle_degrees` | `float` | Rotation angle (CCW) | - -### entity_scale -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity handle | -| `base_x, base_y` | `float` | Scale base point | -| `factor` | `float` | Scale factor | - -### entity_mirror -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity handle | -| `x1, y1` | `float` | Mirror line point 1 | -| `x2, y2` | `float` | Mirror line point 2 | - -### entity_offset -Works on line, circle, polyline. - -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity handle | -| `distance` | `float` | Offset distance (positive = outward) | - -### entity_delete -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity to delete | - -### entity_array_rectangular -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity to array | -| `rows` | `int` | Number of rows | -| `cols` | `int` | Number of columns | -| `row_spacing` | `float` | Row spacing | -| `col_spacing` | `float` | Column spacing | - -Returns list of new entity handles. - -### entity_array_polar -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity to array | -| `center_x, center_y` | `float` | Array center point | -| `count` | `int` | Number of copies | -| `angle` | `float` | Total angle to fill (degrees, default: 360) | - -### entity_set_properties -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity handle | -| `layer` | `str \| null` | Move to layer | -| `color` | `int \| null` | Change ACI color | -| `linetype` | `str \| null` | Change linetype | -| `lineweight` | `float \| null` | Change lineweight | - -All property params are optional — only provided ones are changed. - ---- - -## Query Tools (3) - -### entity_get -| Param | Type | Description | -|-------|------|-------------| -| `handle` | `str` | Entity handle | - -Returns full entity info: `{handle, type, layer, color, linetype, visible, properties}`. - -### entity_list -| Param | Type | Description | -|-------|------|-------------| -| `type_filter` | `str \| null` | Filter: `LINE`, `CIRCLE`, `ARC`, `LWPOLYLINE`, `TEXT`, `MTEXT`, `INSERT`, `HATCH`, etc. | -| `layer_filter` | `str \| null` | Filter by layer name | -| `limit` | `int` | Max results (default: 100) | -| `offset` | `int` | Skip first N results (default: 0) | - -### entity_delete_many -| Param | Type | Description | -|-------|------|-------------| -| `handles` | `list[str]` | List of handles to delete | - -Reports progress. Returns `{deleted, failed, errors}`. diff --git a/.cursor/skills/autocad-mcp/references/tools-layers.md b/.cursor/skills/autocad-mcp/references/tools-layers.md deleted file mode 100644 index 5ba4744..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-layers.md +++ /dev/null @@ -1,63 +0,0 @@ -# Layer Management Tools (12) - -Layers organize entities by category (walls, doors, dimensions, etc.). Each layer has: name, color (ACI), linetype, lineweight, and states (on/off, frozen/thawed, locked/unlocked). - ---- - -## layer_list -No parameters. Returns list of all layers with properties: `{name, color, linetype, lineweight, is_on, is_frozen, is_locked, is_current}`. - -## layer_create -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | New layer name | -| `color` | `int` | ACI color (default: 7=white) | -| `linetype` | `str` | Linetype name (default: `"Continuous"`) | - -## layer_delete -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Layer to delete (must be empty, cannot be layer "0") | - -## layer_set_current -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Layer to make current (new entities go here) | - -## layer_modify -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Layer to modify | -| `color` | `int \| null` | New color | -| `linetype` | `str \| null` | New linetype | -| `lineweight` | `float \| null` | New lineweight | - -## layer_freeze / layer_thaw -Freeze hides and excludes from regeneration (faster). Thaw restores. - -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Layer name | - -## layer_lock / layer_unlock -Lock prevents editing but keeps visible. Unlock restores editing. - -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Layer name | - -## layer_hide / layer_show -Hide turns layer off (invisible). Show turns it on. - -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Layer name | - -## layer_isolate -Shows only the specified layer, hides all others. - -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Layer to keep visible | - -To restore: `layer_show` each hidden layer, or reopen the drawing. diff --git a/.cursor/skills/autocad-mcp/references/tools-view-system.md b/.cursor/skills/autocad-mcp/references/tools-view-system.md deleted file mode 100644 index 1bd8cd2..0000000 --- a/.cursor/skills/autocad-mcp/references/tools-view-system.md +++ /dev/null @@ -1,95 +0,0 @@ -# View, Transaction & System Tools (14) - -## View & Screenshot (5) - -### view_zoom_extents -Zoom to show all entities. No parameters. **COM only** (no-op in ezdxf). - -### view_zoom_window -Zoom to a specific rectangular area. **COM only**. - -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float` | Window corner 1 | -| `x2, y2` | `float` | Window corner 2 | - -### view_screenshot -Capture drawing as image. Returns base64-encoded image. - -- **COM**: Win32 window capture (requires Pillow) -- **ezdxf**: matplotlib render (requires matplotlib) - -### view_zoom_and_screenshot -Combined zoom + screenshot in one call. - -| Param | Type | Description | -|-------|------|-------------| -| `x1, y1` | `float \| null` | Optional zoom window corner 1 | -| `x2, y2` | `float \| null` | Optional zoom window corner 2 | - -If coordinates omitted, captures full extents. - -### view_set_view -Set named view. Parameters depend on backend. - ---- - -## Transactions (3) - -Use transactions to group operations with undo support. - -``` -transaction_begin() # Save checkpoint -... make changes ... -transaction_commit() # Accept changes -# or -transaction_rollback() # Revert to checkpoint -``` - -### transaction_begin -Start a transaction. ezdxf saves full DXF snapshot; COM creates undo mark. - -### transaction_commit -Accept all changes since `transaction_begin`. - -### transaction_rollback -Revert all changes since `transaction_begin`. ezdxf restores snapshot; COM uses native undo. - ---- - -## System (6) - -### system_status -No parameters. Returns server status: `{backend, drawing_loaded, entity_count, uptime, ...}`. - -### system_get_variable -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Variable name: `DIMSCALE`, `LTSCALE`, `INSUNITS`, `CLAYER`, `MEASUREMENT` | - -### system_set_variable -| Param | Type | Description | -|-------|------|-------------| -| `name` | `str` | Variable name | -| `value` | `any` | New value | - -### system_run_command -Execute raw AutoCAD command. **COM only**. Sanitized by `sanitize_command()`. - -| Param | Type | Description | -|-------|------|-------------| -| `command` | `str` | Command string (e.g. `_ZOOM E`, `_REGEN`) | - -Dangerous commands blocked unless `DANGEROUS_COMMANDS_ENABLED=true`. - -### system_run_lisp -Evaluate AutoLISP expression. **COM only**. Sanitized by `sanitize_lisp()`. - -| Param | Type | Description | -|-------|------|-------------| -| `expression` | `str` | LISP expression (e.g. `(command "ZOOM" "E")`) | - -Dangerous functions blocked: `startapp`, `vl-file-delete`, `dos_*`. - -### system_about -No parameters. Returns server capabilities, tool count, version, and feature list. diff --git a/.cursor/skills/autocad-mcp/references/troubleshooting.md b/.cursor/skills/autocad-mcp/references/troubleshooting.md deleted file mode 100644 index 58c2db5..0000000 --- a/.cursor/skills/autocad-mcp/references/troubleshooting.md +++ /dev/null @@ -1,79 +0,0 @@ -# Troubleshooting - -## Common Errors - -### "AutoCAD backend unavailable" -**Cause**: No drawing is loaded — backend needs an active drawing context. -**Fix**: Call `drawing_new()` or `drawing_open(path)` before any other operations. - -### "Entity handle 'XXXX' not found" -**Cause**: Handle doesn't exist, was deleted, or drawing was reloaded. -**Fix**: Use `entity_list()` to get current valid handles. Handles are session-specific in ezdxf; persist by saving/reopening. - -### "Layer 'X' not found" -**Cause**: Layer doesn't exist in the drawing. -**Fix**: `layer_list()` to see existing layers. `layer_create("X")` to create it. - -### "Cannot delete layer '0'" -**Cause**: Layer "0" is the default layer and cannot be removed. -**Fix**: Move entities to another layer instead. Layer "0" always exists. - -### "Path validation failed" -**Cause**: Path contains traversal (`..`) or is outside `ALLOWED_PATHS`. -**Fix**: Use absolute paths within configured allowed directories. Check `ALLOWED_PATHS` env var. - -### "Command rejected by sanitizer" -**Cause**: `system_run_command` detected a dangerous pattern (ERASE, DELETE, etc.). -**Fix**: Use specific tools instead (e.g., `entity_delete` instead of `ERASE` command). Or set `DANGEROUS_COMMANDS_ENABLED=true` if needed. - -### "LISP expression rejected" -**Cause**: `system_run_lisp` detected dangerous function (startapp, file ops). -**Fix**: Use MCP tools instead of LISP for file operations. - -### "COM backend initialization failed" -**Cause**: AutoCAD not running, not installed, or COM registration issues. -**Fix**: Start AutoCAD, then retry. Or switch to ezdxf: `AUTOCAD_MCP_BACKEND=ezdxf`. - -### "View operations are no-ops" -**Cause**: Using ezdxf backend — view/zoom commands only work with COM. -**Fix**: Switch to COM backend if live viewport control is needed. Screenshots still work via matplotlib in ezdxf. - -### "Screenshot failed" -**Cause**: Missing dependency (Pillow for COM, matplotlib for ezdxf). -**Fix**: `pip install Pillow` (COM) or `pip install matplotlib` (ezdxf). - -### "PDF export failed" -**Cause**: matplotlib not installed. -**Fix**: `pip install matplotlib` or `pip install -e ".[pdf]"`. - -### "Transaction not active" -**Cause**: `transaction_commit/rollback` called without `transaction_begin`. -**Fix**: Always call `transaction_begin()` first. - -### "Block 'X' not found" -**Cause**: Block definition doesn't exist. -**Fix**: `block_list()` to see available blocks. Create a block with `block_create_from_entities()`. - ---- - -## Backend-Specific Issues - -### ezdxf Backend -- **No DWG write support**: ezdxf reads DWG but can only write DXF. Use `drawing_export_dxf()`. -- **Undo stack memory**: Full DXF snapshots stored. `MAX_UNDO_STACK` env limits count (default: 50). -- **Text rendering**: Depends on system fonts. MathTex/complex formatting may not render in screenshots. - -### COM Backend -- **STA threading**: All COM calls go through a single thread. Batch operations are sequential. -- **AutoCAD version**: Tested with AutoCAD 2020+. Older versions may have COM API differences. -- **Window capture**: Screenshots capture the AutoCAD window as-is. Minimize overlapping windows. - ---- - -## Diagnostic Steps - -1. `system_status()` — Check backend type, connection, drawing state -2. `system_about()` — See available tools and capabilities -3. `drawing_info()` — Verify drawing is loaded and get metadata -4. `analysis_entity_stats()` — Quick overview of drawing contents -5. Check logs: `LOG_LEVEL=DEBUG` for verbose output diff --git a/.cursor/skills/autocad-mcp/references/workflows.md b/.cursor/skills/autocad-mcp/references/workflows.md deleted file mode 100644 index a7c65dc..0000000 --- a/.cursor/skills/autocad-mcp/references/workflows.md +++ /dev/null @@ -1,166 +0,0 @@ -# Common Workflows - -## 1. New Drawing from Scratch - -``` -Progress: -- [ ] Step 1: Create drawing -- [ ] Step 2: Set up layers -- [ ] Step 3: Draw entities -- [ ] Step 4: Add dimensions -- [ ] Step 5: Add text annotations -- [ ] Step 6: Verify and save -``` - -**Step 1**: `drawing_new()` — creates empty drawing - -**Step 2**: Create layers for organization: -``` -layer_create("WALLS", color=7, linetype="Continuous") -layer_create("DOORS", color=3) -layer_create("WINDOWS", color=5) -layer_create("DIMENSIONS", color=1) -layer_create("TEXT", color=2) -``` -Or use `template_apply_layers("architectural")` for standard sets. - -**Step 3**: Draw geometry on appropriate layers: -``` -layer_set_current("WALLS") -entity_create_rectangle(x1=0, y1=0, x2=6000, y2=4000) -entity_create_line(x1=3000, y1=0, x2=3000, y2=4000) -``` - -**Step 4**: Add dimensions: -``` -layer_set_current("DIMENSIONS") -dimension_linear(x1=0, y1=0, x2=6000, y2=0, dim_x=3000, dim_y=-500) -``` - -**Step 5**: Add text labels: -``` -layer_set_current("TEXT") -entity_create_text(text="Living Room", x=1500, y=2000, height=200) -``` - -**Step 6**: Verify and save: -``` -analysis_entity_stats() # check entity counts -validation_check() # run quality checks -drawing_save("/path/to/drawing.dxf") -``` - ---- - -## 2. Edit Existing DXF - -``` -Progress: -- [ ] Step 1: Open file -- [ ] Step 2: Explore contents -- [ ] Step 3: Make changes (with transaction) -- [ ] Step 4: Verify and save -``` - -**Step 1**: `drawing_open("/path/to/file.dxf")` - -**Step 2**: Explore: -``` -analysis_entity_stats() # overview -layer_list() # see layers -entity_list(type_filter="LINE", limit=20) # browse entities -``` - -**Step 3**: Begin transaction, then modify: -``` -transaction_begin() -entity_move(handle="1A2B", dx=100, dy=0) -entity_set_properties(handle="3C4D", color=1, layer="WALLS") -entity_delete(handle="5E6F") -transaction_commit() # or transaction_rollback() to undo all -``` - -**Step 4**: `drawing_save()` - ---- - -## 3. Batch Entity Creation - -``` -Progress: -- [ ] Step 1: Prepare entity list -- [ ] Step 2: Batch create -- [ ] Step 3: Verify results -``` - -**Step 1**: Build entity definitions: -```json -[ - {"type": "line", "x1": 0, "y1": 0, "x2": 100, "y2": 0, "layer": "WALLS"}, - {"type": "line", "x1": 100, "y1": 0, "x2": 100, "y2": 100, "layer": "WALLS"}, - {"type": "circle", "cx": 50, "cy": 50, "radius": 10, "layer": "DETAILS"}, - {"type": "text", "text": "A1", "x": 50, "y": -20, "height": 5, "layer": "TEXT"} -] -``` - -**Step 2**: `entity_batch_create(entities=[...])` — returns handles for all created entities - -**Step 3**: `analysis_entity_stats()` to verify counts - ---- - -## 4. Drawing Analysis - -``` -Progress: -- [ ] Step 1: Open drawing -- [ ] Step 2: Get overview stats -- [ ] Step 3: Examine specific areas -- [ ] Step 4: Measure and report -``` - -**Step 1**: `drawing_open(path)` or `drawing_info()` if already open - -**Step 2**: -``` -analysis_entity_stats() # by type and layer -analysis_layer_stats() # per-layer details -analysis_bounding_box() # drawing extents -``` - -**Step 3**: Region queries: -``` -analysis_find_in_region(x1=0, y1=0, x2=1000, y2=1000, type_filter="LINE") -analysis_select_by_layer(layer_name="WALLS") -analysis_select_by_type(entity_type="CIRCLE") -``` - -**Step 4**: Measurements: -``` -analysis_measure_distance(x1=0, y1=0, x2=100, y2=100) -analysis_measure_area(points=[[0,0],[100,0],[100,100],[0,100]]) -``` - ---- - -## 5. Template-Based Drawing - -``` -Progress: -- [ ] Step 1: Create drawing with template -- [ ] Step 2: Apply layer standards -- [ ] Step 3: Add content -- [ ] Step 4: Validate and save -``` - -**Step 1**: `drawing_new(template="/path/to/template.dwt")` (or `drawing_new()` without template) - -**Step 2**: `template_apply_layers("architectural")` — creates standard layers - -**Step 3**: Draw content on appropriate layers - -**Step 4**: -``` -validation_check(["empty_layers", "zero_length"]) -drawing_save("/path/to/output.dxf") -``` diff --git a/.cursor/skills/autocad-mcp/scripts/check_setup.py b/.cursor/skills/autocad-mcp/scripts/check_setup.py deleted file mode 100644 index c9feeb8..0000000 --- a/.cursor/skills/autocad-mcp/scripts/check_setup.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Check AutoCAD MCP Pro setup and dependencies.""" - -import importlib -import os -import sys - - -def check(name: str, condition: bool, fix: str = ""): - status = "OK" if condition else "MISSING" - symbol = "[+]" if condition else "[-]" - line = f" {symbol} {name}: {status}" - if not condition and fix: - line += f" -> {fix}" - print(line) - return condition - - -def main(): - print("AutoCAD MCP Pro — Setup Check\n") - ok_count = 0 - total = 0 - - print("Python:") - total += 1 - v = sys.version_info - ok_count += check( - f"Python {v.major}.{v.minor}.{v.micro}", - v >= (3, 11), - "Requires Python 3.11+", - ) - - print("\nRequired packages:") - for pkg, install in [ - ("fastmcp", "pip install fastmcp"), - ("ezdxf", "pip install ezdxf"), - ("pydantic", "pip install pydantic"), - ]: - total += 1 - try: - importlib.import_module(pkg) - ok_count += check(pkg, True) - except ImportError: - ok_count += check(pkg, False, install) - - print("\nOptional packages:") - for pkg, install, purpose in [ - ("win32com", "pip install pywin32", "COM backend"), - ("PIL", "pip install Pillow", "Screenshots (COM)"), - ("matplotlib", "pip install matplotlib", "PDF export + screenshots (ezdxf)"), - ]: - total += 1 - try: - importlib.import_module(pkg) - ok_count += check(f"{pkg} ({purpose})", True) - except ImportError: - ok_count += check(f"{pkg} ({purpose})", False, install) - - print("\nEnvironment variables:") - for var, default in [ - ("AUTOCAD_MCP_BACKEND", "auto"), - ("ALLOWED_PATHS", "(empty = all paths allowed)"), - ("MAX_UNDO_STACK", "50"), - ("LOG_LEVEL", "INFO"), - ("DANGEROUS_COMMANDS_ENABLED", "false"), - ]: - val = os.environ.get(var, "") - total += 1 - ok_count += check(var, True, "") - if val: - print(f" Current value: {val}") - else: - print(f" Default: {default}") - - print(f"\nResult: {ok_count}/{total} checks passed") - return 0 if ok_count >= 4 else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.cursor/skills/autocad-mcp/scripts/validate_drawing.py b/.cursor/skills/autocad-mcp/scripts/validate_drawing.py deleted file mode 100644 index 79c5f86..0000000 --- a/.cursor/skills/autocad-mcp/scripts/validate_drawing.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Validate drawing quality — run after creating/editing a drawing. - -Usage: python scripts/validate_drawing.py [drawing_path] - -This script checks for common drawing issues: -- Empty layers (layers with no entities) -- Entities on layer "0" (should use named layers) -- Entity count summary -""" - -import json -import sys - - -def validate_stats(stats: dict) -> list[str]: - """Validate entity statistics. Pass the result of analysis_entity_stats().""" - issues = [] - - if stats.get("total", 0) == 0: - issues.append("WARNING: Drawing has no entities") - - by_layer = stats.get("by_layer", {}) - if "0" in by_layer and by_layer["0"] > 0: - issues.append( - f"INFO: {by_layer['0']} entities on default layer '0' " - "(consider moving to named layers)" - ) - - return issues - - -def validate_layers(layers: list[dict]) -> list[str]: - """Validate layer configuration. Pass the result of layer_list().""" - issues = [] - - for layer in layers: - name = layer.get("name", "") - if name == "0": - continue - if layer.get("color", 7) == 7 and name != "0": - issues.append( - f"INFO: Layer '{name}' uses default color (white/7). " - "Consider assigning a distinct color." - ) - - return issues - - -def main(): - print("Drawing Validation Tool") - print("=" * 40) - print() - print("This tool validates output from MCP analysis tools.") - print("Pipe JSON from analysis_entity_stats() or layer_list().") - print() - print("Usage with MCP:") - print(" 1. Call analysis_entity_stats() → copy JSON output") - print(" 2. Call layer_list() → copy JSON output") - print(" 3. Review results for issues") - print() - print("Checks performed:") - print(" - Empty drawing detection") - print(" - Entities on default layer '0'") - print(" - Layers with default color") - print() - - if not sys.stdin.isatty(): - try: - data = json.load(sys.stdin) - if "total" in data: - issues = validate_stats(data) - elif isinstance(data, list): - issues = validate_layers(data) - else: - issues = ["ERROR: Unrecognized input format"] - - if issues: - for issue in issues: - print(f" {issue}") - else: - print(" OK — No issues found") - except json.JSONDecodeError: - print(" ERROR: Invalid JSON input") - else: - print("No stdin input. Run interactively or pipe JSON data.") - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fd93fc..ec3ef12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,6 @@ on: jobs: lint: runs-on: ubuntu-latest - # NOTE: non-blocking until the repo-wide ruff cleanup lands (tracked for the - # quality pass). The test job below is the real gate. Once the codebase is - # ruff-clean, drop `continue-on-error` to make lint blocking again. - continue-on-error: true steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/backends/__init__.py b/backends/__init__.py index 4a3165f..4779698 100644 --- a/backends/__init__.py +++ b/backends/__init__.py @@ -1,11 +1,10 @@ """AutoCAD MCP Pro – backend package.""" -from .base import AutoCADBackend, BlockInfo, CommandResult, DrawingInfo, EntityInfo, LayerInfo +from .base import AutoCADBackend, BlockInfo, DrawingInfo, EntityInfo, LayerInfo from .com_backend import ComBackend from .ezdxf_backend import EzdxfBackend __all__ = [ "AutoCADBackend", - "CommandResult", "EntityInfo", "LayerInfo", "BlockInfo", diff --git a/backends/base.py b/backends/base.py index df453b1..c62b057 100644 --- a/backends/base.py +++ b/backends/base.py @@ -5,7 +5,7 @@ from __future__ import annotations import math from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from engineering.plan_spec import ( @@ -23,21 +23,6 @@ if TYPE_CHECKING: # --------------------------------------------------------------------------- -@dataclass -class CommandResult: - ok: bool - payload: Any = None - error: str | None = None - - def to_dict(self) -> dict: - d: dict[str, Any] = {"ok": self.ok} - if self.ok: - d["payload"] = self.payload - else: - d["error"] = self.error - return d - - @dataclass class EntityInfo: handle: str @@ -91,7 +76,7 @@ class DrawingInfo: # --------------------------------------------------------------------------- -def normalize_lineweight(lw: "float | int | None") -> "int | None": +def normalize_lineweight(lw: float | int | None) -> int | None: """Coerce a lineweight to ezdxf/COM hundredths-of-a-mm integers. AutoCAD and ezdxf store lineweights as integer hundredths (25 == 0.25 mm), @@ -548,17 +533,17 @@ class AutoCADBackend(ABC): # behaviour. The only backend-specific primitive is `_create_xline` — XLINE # has no cross-backend equivalent among the standard entity_create_* tools. - _plan_spec: "PlanSpec | None" = None + _plan_spec: PlanSpec | None = None async def drawing_plan( self, intent: str, - sheet_size: "SheetSize" = "A3", + sheet_size: SheetSize = "A3", scale: float = 1.0, - layer_set_id: "LayerSetId" = "mech", + layer_set_id: LayerSetId = "mech", view_count: int = 1, - dim_style: "DimStyle" = "chain", + dim_style: DimStyle = "chain", notes: list[str] | None = None, - ) -> "PlanSpec": + ) -> PlanSpec: """Commit a drawing intent before any geometry is created. Returns a PlanSpec held on the backend for `drawing_critique`.""" from engineering.plan_spec import PlanSpec @@ -575,15 +560,15 @@ class AutoCADBackend(ABC): return plan async def drawing_critique( - self, focus: list["CritiqueFocus"] | None = None, - ) -> list["Issue"]: + self, focus: list[CritiqueFocus] | None = None, + ) -> list[Issue]: """Run premium-quality checks; return zero issues before `drawing_finalize`. `focus=None` runs all checks.""" from engineering.critique import run_critique return await run_critique(self, focus) async def point_from_snap( - self, handle: str, snap: "SnapType", + self, handle: str, snap: SnapType, ref_x: float | None = None, ref_y: float | None = None, ) -> tuple[float, float]: """Return a deterministic snap point on `handle`. @@ -769,7 +754,7 @@ class AutoCADBackend(ABC): return {"ok": True, "layer": layer, "deleted": deleted} async def drawing_apply_iso_layers( - self, standard: "LayerSetId" = "mech", + self, standard: LayerSetId = "mech", ) -> dict: """Bootstrap a full ISO-conformant layer set with correct colors and lineweights. Idempotent.""" @@ -778,7 +763,7 @@ class AutoCADBackend(ABC): return {"ok": True, "standard": standard, "layers": result} async def dimension_auto( - self, handles: list[str], style: "DimStyle" = "chain", + self, handles: list[str], style: DimStyle = "chain", offset: float = 10.0, ) -> list[EntityInfo]: """Generate ISO 129 dimensions across `handles` in the chosen style @@ -828,7 +813,7 @@ class AutoCADBackend(ABC): dx, dy = ex0 - sx0, ey0 - sy0 L0 = math.hypot(dx, dy) or 1.0 ux, uy = dx / L0, dy / L0 - for i, (sx, sy, ex, ey) in enumerate(segs): + for i, (_sx, _sy, ex, ey) in enumerate(segs): dim = await self.dimension_linear( sx0, sy0, ex, ey, sx0 - uy * (off + i * off), sy0 + ux * (off + i * off), @@ -906,32 +891,6 @@ class AutoCADBackend(ABC): return [e for e in ents if _ok(e)] - # ── concrete helpers (use existing primitives) ────────────────────────── - async def set_layer_active(self, name: str) -> None: - """Convenience: alias for layer_set_current. Concrete; backends need not override.""" - await self.layer_set_current(name) - - async def ensure_linetypes( - self, names: list[str], file: str | None = None, - ) -> dict[str, str]: - """Idempotently load each linetype in `names`. Returns {name: status}. - Status is one of: 'already_loaded', 'loaded', 'failed: '. - """ - try: - current = {ln.lower() for ln in await self.linetype_list()} - except Exception: - current = set() - results: dict[str, str] = {} - for n in names: - if n.lower() in current: - results[n] = "already_loaded" - continue - try: - await self.linetype_load(n, file=file) - results[n] = "loaded" - except Exception as exc: - results[n] = f"failed: {exc}" - return results # --------------------------------------------------------------------------- diff --git a/backends/com_backend.py b/backends/com_backend.py index 6115899..0d9467e 100644 --- a/backends/com_backend.py +++ b/backends/com_backend.py @@ -360,7 +360,6 @@ class ComBackend(AutoCADBackend): def __init__(self): self._executor: ThreadPoolExecutor | None = None - self._batch_mode = False self._connected = False self._transaction_active = False @@ -413,7 +412,7 @@ class ComBackend(AutoCADBackend): if timeout > 0: return await asyncio.wait_for(future, timeout=timeout) return await future - except asyncio.TimeoutError as e: + except TimeoutError as e: log.error("COM call timed out after %.1fs; rebuilding executor", timeout) # The worker thread is still blocked inside SendCommand and cannot be # cancelled. Abandon it (shutdown(wait=False)) and start a fresh @@ -656,7 +655,7 @@ class ComBackend(AutoCADBackend): def _sync(): mspace = _msp() # acPatternTypePreDefined = 0 - hatch = mspace.AddHatch(0, pattern, True) + hatch = mspace.AddHatch(0, pattern, False) hatch.PatternScale = float(scale) hatch.PatternAngle = deg2rad(angle) # Build outer loop as a temporary lwpolyline @@ -886,7 +885,6 @@ class ComBackend(AutoCADBackend): self, handle, rows, cols, row_spacing, col_spacing, ) -> list[EntityInfo]: def _sync(): - self._batch_mode = True _COM_STATE["batch_mode"] = True try: doc = _acad_doc() @@ -904,7 +902,6 @@ class ComBackend(AutoCADBackend): entities.append(_entity_info(result)) return entities finally: - self._batch_mode = False _COM_STATE["batch_mode"] = False _regen() return await self._run(_sync) @@ -913,7 +910,6 @@ class ComBackend(AutoCADBackend): self, handle, count, fill_angle, center_x, center_y, ) -> list[EntityInfo]: def _sync(): - self._batch_mode = True _COM_STATE["batch_mode"] = True try: doc = _acad_doc() @@ -930,7 +926,6 @@ class ComBackend(AutoCADBackend): entities.append(_entity_info(result)) return entities finally: - self._batch_mode = False _COM_STATE["batch_mode"] = False _regen() return await self._run(_sync) diff --git a/backends/ezdxf_backend.py b/backends/ezdxf_backend.py index c707c12..edeb6b2 100644 --- a/backends/ezdxf_backend.py +++ b/backends/ezdxf_backend.py @@ -51,11 +51,6 @@ except ImportError: # Helper utilities # --------------------------------------------------------------------------- -_ACI_NAMES = { - 1: "Red", 2: "Yellow", 3: "Green", 4: "Cyan", - 5: "Blue", 6: "Magenta", 7: "White", 256: "ByLayer", 0: "ByBlock", -} - _BUILTIN_LINETYPES = {"continuous", "bylayer", "byblock"} # AutoCAD-shipped linetypes that ezdxf.tools.standards does not include. @@ -344,12 +339,17 @@ class EzdxfBackend(AutoCADBackend): return await self._async(_sync) async def drawing_save_as(self, path: str, fmt: str = "dxf") -> dict: + if fmt.lower() == "dwg": + raise RuntimeError( + "ezdxf backend cannot write true DWG binary. " + "Use save_path with a .dxf extension, or switch to the COM backend." + ) def _sync(): doc = self._require_doc() doc.saveas(path) self._doc_path = path self._dirty = False - return {"ok": True, "path": path, "format": fmt} + return {"ok": True, "path": path, "format": "dxf"} return await self._async(_sync) async def drawing_export_dxf(self, path: str) -> dict: @@ -693,7 +693,7 @@ class EzdxfBackend(AutoCADBackend): dim = msp.add_aligned_dim( p1=(float(x1), float(y1)), p2=(float(x2), float(y2)), - dist=math.sqrt((float(dim_x) - float(x1)) ** 2 + (float(dim_y) - float(y1)) ** 2), + distance=math.sqrt((float(dim_x) - float(x1)) ** 2 + (float(dim_y) - float(y1)) ** 2), ) dim.render() ent = dim.dimension @@ -708,11 +708,12 @@ class EzdxfBackend(AutoCADBackend): ) -> EntityInfo: def _sync(): msp = self._msp() + vxf, vyf = float(vx), float(vy) dim = msp.add_angular_dim_2l( - center=(float(vx), float(vy)), - p1=(float(x1), float(y1)), - p2=(float(x2), float(y2)), - distance=10.0, + base=(float(tx), float(ty)), + line1=((vxf, vyf), (float(x1), float(y1))), + line2=((vxf, vyf), (float(x2), float(y2))), + location=(float(tx), float(ty)), ) dim.render() ent = dim.dimension @@ -875,11 +876,19 @@ class EzdxfBackend(AutoCADBackend): d = float(distance) if ent_type == "LINE": - # Offset a line: move parallel + # Offset a line: move parallel; side_x/side_y selects which side start = Vec2(ent.dxf.start.x, ent.dxf.start.y) end = Vec2(ent.dxf.end.x, ent.dxf.end.y) direction = (end - start).normalize() - normal = Vec2(-direction.y, direction.x) * d + left_normal = Vec2(-direction.y, direction.x) + # Determine sign: positive = left side (default); flip if side point is on right + sign = 1.0 + if side_x is not None and side_y is not None: + mid = (start + end) * 0.5 + side_vec = Vec2(float(side_x), float(side_y)) - mid + if left_normal.dot(side_vec) < 0: + sign = -1.0 + normal = left_normal * (d * sign) new_start = start + normal new_end = end + normal msp = self._msp() @@ -893,7 +902,13 @@ class EzdxfBackend(AutoCADBackend): elif ent_type == "CIRCLE": cx, cy = ent.dxf.center.x, ent.dxf.center.y - new_r = ent.dxf.radius + d + # side_x/side_y selects inside (shrink) or outside (grow); default = outside + sign = 1.0 + if side_x is not None and side_y is not None: + dist_to_center = math.sqrt((float(side_x) - cx) ** 2 + (float(side_y) - cy) ** 2) + if dist_to_center < ent.dxf.radius: + sign = -1.0 + new_r = ent.dxf.radius + d * sign if new_r <= 0: raise ValueError("Offset distance too large for circle") msp = self._msp() @@ -944,8 +959,12 @@ class EzdxfBackend(AutoCADBackend): from ezdxf.math import Matrix44 results = [] cx, cy = float(center_x), float(center_y) - step = math.radians(float(fill_angle)) / max(int(count) - 1, 1) - for i in range(1, int(count)): + fa = float(fill_angle) + n = int(count) + # 360° full circle: n copies evenly spaced, no duplicate at 0° + divisor = n if abs(fa % 360.0) < 1e-6 else max(n - 1, 1) + step = math.radians(fa) / divisor + for i in range(1, n): copy = ent.copy() msp.add_entity(copy) angle = step * i @@ -1535,7 +1554,7 @@ class EzdxfBackend(AutoCADBackend): async def entity_trim(self, target_handle, cutter_handle, keep_x, keep_y) -> EntityInfo: def _sync(): - from ezdxf.math import intersection_line_line_2d, Vec2 + from ezdxf.math import Vec2, intersection_line_line_2d target = self._get_entity(target_handle) cutter = self._get_entity(cutter_handle) if target.dxftype() != "LINE" or cutter.dxftype() != "LINE": @@ -1574,7 +1593,7 @@ class EzdxfBackend(AutoCADBackend): self, target_handle, boundary_handle, end_x=None, end_y=None, ) -> EntityInfo: def _sync(): - from ezdxf.math import intersection_line_line_2d, Vec2 + from ezdxf.math import Vec2, intersection_line_line_2d target = self._get_entity(target_handle) boundary = self._get_entity(boundary_handle) if target.dxftype() != "LINE" or boundary.dxftype() != "LINE": @@ -1616,7 +1635,7 @@ class EzdxfBackend(AutoCADBackend): """Shared geometry setup for fillet/chamfer. Returns (line1, line2, P, d1, d2, A_far, B_far, theta) where d1/d2 are unit vectors from intersection P toward the far endpoint of each line.""" - from ezdxf.math import intersection_line_line_2d, Vec2 + from ezdxf.math import Vec2, intersection_line_line_2d line1 = self._get_entity(handle1) line2 = self._get_entity(handle2) if line1.dxftype() != "LINE" or line2.dxftype() != "LINE": diff --git a/docs/analysis/00-RECON-dossier.md b/docs/analysis/00-RECON-dossier.md new file mode 100644 index 0000000..8cb3cfc --- /dev/null +++ b/docs/analysis/00-RECON-dossier.md @@ -0,0 +1,209 @@ +The lineweight truncation bug is confirmed devastating: ENGINEERING_LAYERS uses floats like 0.50, 0.25, 0.18, 0.13, 0.05 mm, and `layer_create` does `int(lineweight)` (ezdxf_backend.py:1034), so every one floors to 0. Counts confirmed: 109 tools, 6 resources, 5 prompts; LICENSE/benchmarks/ci.yml all absent. All load-bearing claims verified against source. I have everything needed to synthesize. + +# AutoCAD MCP Pro — Understanding & Research Dossier + +## 1. Architecture at a Glance + +AutoCAD MCP Pro is a **single-file FastMCP 3.0 server** (`server.py`, 2881 lines) exposing **109 `@mcp.tool` registrations, 6 `@mcp.resource` endpoints, 5 `@mcp.prompt` templates** (verified by grep, not the docs' "87"). It uses a **strategy-pattern dual backend** behind one ABC (`backends/base.py`, ~73 abstract methods + 5 dataclasses): a **live COM backend** (`com_backend.py`, pywin32, single STA-thread `ThreadPoolExecutor`) and a **headless ezdxf backend** (`ezdxf_backend.py`, file-based DXF, all sync work via `asyncio.to_thread` under one `asyncio.Lock`). Backend selection (`server.py::_make_backend`, server.py:69): env `AUTOCAD_MCP_BACKEND`; Windows `auto`/`com` tries COM then falls back to ezdxf; non-Windows always ezdxf. + +Request path: `ErrorHandlingMiddleware → AuditMiddleware → TimingMiddleware → LoggingMiddleware` (server.py:197-200) → tool fn → `_backend(ctx)` (server.py:207, raises `ToolError` if backend None) → identically-named backend method → `_dc()` dataclass→dict serialization (server.py:216). Lifespan (`autocad_lifespan`, server.py:107) builds the backend singleton into `ctx.lifespan_context["backend"]`. + +A **deterministic engineering layer** (`engineering/` package) backs the "premium" workflow: parametric gear/keyway/titleblock primitives, a closed-enum `drawing_critique`, an 8-step `DrawingValidator`, ISO layer/linetype scaffolds, and a `PlanSpec` plan-before-draw contract. **The central architectural fact: the premium workflow is fully implemented only on ezdxf; the COM backend stubs all 8 premium meta-tools with `NotImplementedError` (com_backend.py:1637-1660, verified) — yet COM is the default Windows backend.** + +--- + +## 2. Per-Subsystem Summary + +**`server.py` (transport + tool registry).** Thin async wrappers over the backend singleton, grouped into 13 sections (1–12 + sub-sections 8b/8c/8d batch/template/validation + 13 premium). Owns server-side composition tools (`entity_delete_many` server.py:1065, `layer_isolate` :1216, `block_find_references` :1362, `analysis_layer_stats` :1506, `entity_batch_*` :1538/:1590, `template_*` :1695/:1730, `validation_check` :1749). Entry point manually parses `--transport/--port/--host` (server.py:2866) and refuses non-loopback HTTP unless `ALLOW_REMOTE_HTTP` + `MCP_AUTH_TOKEN` (`_validate_http_bind`). + +**`backends/base.py` (the contract).** `AutoCADBackend` ABC: ~73 `@abstractmethod`, 2 concrete (`set_layer_active`, `ensure_linetypes`), 5 dataclasses (`EntityInfo/LayerInfo/BlockInfo/DrawingInfo/CommandResult`), 3 helpers (`shoelace_area`, `deg2rad`, `rad2deg`). Both backends define a body for every abstract method (no structural gaps); divergences are behavioral. + +**`backends/ezdxf_backend.py` (headless).** Full create/modify/query/analysis/dimension/block/layer + premium meta-tools + corner ops. Transactions = full DXF snapshot to tempfile on `_undo_stack` (trimmed to `max_undo_stack=5`, O(drawing-size) per `transaction_begin`). View ops and `system_run_command/run_lisp` are documented no-ops. Screenshot/PDF via matplotlib. + +**`backends/com_backend.py` (live AutoCAD).** All COM through one STA worker with per-call timeout (`com_call_timeout`, default 60s) and executor-rebuild on timeout/fatal HRESULT. Corner ops via `_safe_send_command` (CMDACTIVE-polling). Win32 `PrintWindow` screenshots. **8 premium meta-tools + `block_create_from_entities` are stubs.** + +**`engineering/` (deterministic CAD).** `gear.py` (involute spur/helical outlines), `keyway.py` (DIN 6885), `titleblock.py` (ISO 7200 A3), `layers.py` (mech/pid/iso13567 sets), `critique.py` (6 focuses), `validator.py` (8-step gate), `plan_spec.py` (PlanSpec/Issue + ISO-128 lineweight set). `section.py` (`cutting_plane_line`, `apply_section_hatch`) and `gear.generate_tooth_profile` are **dead code** (exported, zero callers/tests). + +**`tests/` (184 functions, 14 files).** One ezdxf-only `backend` fixture (`conftest.py`). COM backend, server→sanitizer wiring, and `section.py` have **zero coverage**. + +--- + +## 3. Complete MCP Tool Inventory (grouped; ✅ real / ⚠️ partial / ❌ stub-or-missing) + +> All 109 tools are **registered** in `server.py` and have **real ezdxf implementations**. "Status" below reflects the **COM backend** (the Windows default) plus correctness caveats. Nothing is purely aspirational/unregistered. + +| § | Tools | ezdxf | COM | Notes | +|---|---|---|---|---| +| 1 Drawing (12, doc says 11) | info, new, open, save, save_as, export_dxf, export_pdf, purge, audit, close, undo, redo | ✅ (redo ❌ no-op ezdxf_backend.py:487) | ✅ | `drawing_redo` inert on ezdxf; header says 11 but 12 registered (drawing_redo server.py:426) | +| 2 Entity Create (13) | line, circle, arc, polyline, rectangle(→polyline), text, mtext, hatch, spline, ellipse, point, block_ref | ✅ | ✅ | mtext has no rotation param on ezdxf (:578); hatch pattern-only single loop | +| 3 Dimensions (5) | linear, aligned, angular, radius, diameter | ⚠️ aligned/angular **xfail** (tests/test_dimensions.py:16); angular ignores tx/ty + fixed dist=10 (ezdxf:705) | ✅ | No dimstyle/tolerance support either backend | +| 4 Modify (14) | move, copy, rotate, scale, mirror, offset, trim, extend, fillet, chamfer, delete, array_rectangular, array_polar, set_properties | ⚠️ offset ignores side (:868); corner ops LINE+LINE only (:1535-1750); polar 360° dup (:946) | ⚠️ offset ignores side (:860); corner ops via SendCommand | | +| 5 Query (3) | entity_get, entity_list, entity_delete_many | ✅ | ✅ | COM entity_list = O(n) per-item COM reads (:967) | +| 6 Layer (14) | list, create, delete, set_current, modify, freeze, thaw, lock, unlock, hide, show, isolate, linetype_list, linetype_load | ⚠️ **lineweight int() truncation** (:1034) | ✅ | See RISK-2 | +| 7 Block (7) | list, insert, explode, get_attributes, set_attributes, create_from_entities, find_references | ✅ | ⚠️ create_from_entities ❌ stub (:1253) | BlockInfo.description never populated | +| 8 Analysis (8) | stats, find_in_region, measure_distance, measure_area, bounding_box, select_by_layer, select_by_type, layer_stats | ⚠️ region fallback insert/start only (:1330) | ✅ | hand-rolled loops vs ezdxf query/groupby | +| 8b Batch (2) | entity_batch_create, entity_batch_modify | ✅ | ✅ | | +| 8c Template (2) | template_apply_layers, template_list | ✅ | ✅ | Duplicates `_LAYER_TEMPLATES`/prompt layer sets | +| 8d Validation (1) | validation_check | ✅ | ✅ | | +| 9 View (4, doc says 5) | zoom_extents, zoom_window, screenshot, zoom_and_screenshot | ⚠️ zoom = no-op (:1394) | ✅ | Header says 5 | +| 10 Transactions (3) | begin, commit, rollback | ✅ (commit file-I/O outside lock :1452) | ⚠️ flag-leak on error (:1402) | | +| 11 System (6) | status, get_variable, set_variable, run_command, run_lisp, about | ⚠️ run_command/lisp no-op | ⚠️ run_lisp result always 'nil' (:1487); about catalog drifted | | +| 12 Engineering (7) | gear_draw_helical/spur/section_aa, keyway_draw_keyed_bore/section, titleblock_apply_iso_a3, drawing_finalize | ✅ | ✅ (use generic entity_create_*) | Validator/titleblock import-guarded → None risk | +| 13 Premium (8) | drawing_plan, drawing_critique, point_from_snap, construction_xline, construction_clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart | ✅ | ❌ **all NotImplementedError** (:1637-1660) | **The documented-mandatory workflow is dead on COM** | + +**Documented-but-missing entirely:** none. **Documented-as-working but stubbed on default backend:** the 8 premium tools (§13) + `block_create_from_entities` on COM. + +--- + +## 4. Cross-Cutting RISK Register (merged, deduped, severity-sorted) + +| # | Sev | Risk | Location | Evidence | +|---|---|---|---|---| +| R1 | **CRITICAL** | All 8 premium meta-tools `raise NotImplementedError` on COM (the Windows default). The "non-negotiable for production" workflow (`drawing_plan→apply_iso_layers→critique→finalize`) 500s on the primary platform. | com_backend.py:1637-1660 | **Verified verbatim**; ezdxf impls real (ezdxf_backend.py:1753-2050) | +| R2 | **CRITICAL** | `layer_create` does `int(lineweight)` so every sub-1.0 ISO mm value (0.50/0.25/0.18/0.13/0.05) floors to **0**. The entire ISO-128 lineweight discipline is silently nulled; `drawing_apply_iso_layers`/`drawing_new` bootstrap produce flat 0-weight layers. | ezdxf_backend.py:1034; values engineering/layers.py:16-26 | **Both verified.** ezdxf wants integer hundredths (50=0.50mm), not raw mm | +| R3 | **HIGH** | `_check_iso128` is a permanent no-op: `lw <= 0: continue` (critique.py:50) skips every layer made 0 by R2. The advertised ISO gate can never fire. Comment "the layer dataclass already normalises to mm" (critique.py:53) is **false** (ezdxf_backend.py:189 stores raw `float(lw)`). | engineering/critique.py:37-64 | **Verified verbatim** | +| R4 | **HIGH** | `_check_dim_overlap` reads `properties['text_position'/'defpoint'/'insertion_point']`, but the ezdxf DIM extractor sets only `dim_type` (ezdxf_backend.py:166-168) → `dim_points` always empty → check is a silent no-op. | engineering/critique.py:194-197 | — | +| R5 | **HIGH** | COM timeout path (`stuck.shutdown(wait=False)`) cannot kill the STA worker still blocked in SendCommand → **permanently leaks one CoInitialize'd thread + COM proxy per timeout**; no `CoUninitialize` anywhere. | com_backend.py:415-433, 68-71 | — | +| R6 | **HIGH** | Zero COM-backend test coverage (~80 methods incl. UndoMark transactions, SendCommand corner ops, CMDACTIVE guard). Passing ezdxf transaction tests give **false confidence** for the semantically different COM model. | tests/ (no ComBackend import); conftest.py:8-15 | — | +| R7 | **HIGH** | Server→sanitizer wiring untested: `test_security.py` tests `sanitize_command/lisp/validate_path` in isolation; no integration test proves `system_run_command/lisp` actually call them. A refactor dropping the call (server.py:2035/2052) passes all security tests while raw commands reach SendCommand. | server.py:2035,2052; tests/test_security.py | — | +| R8 | **HIGH** | LISP-aliasing bypass (`(setq f command)(f "ERASE")`) is claimed-defended in a comment (security.py:138-140) but has **no adversarial test**; UNC/`~`/symlink path escapes in `validate_path` also untested. | security.py:138-179; test_security.py:177-231 | — | +| R9 | **MED** | `entity_array_polar` full-360° fill double-places: `step=radians(360)/(count-1)` puts the last copy at 0°==original → duplicate. Hits the common bolt-circle/gear pattern the engineering layer relies on. | ezdxf_backend.py:946-947 | — | +| R10 | **MED** | `drawing_finalize` calls `drawing_save_as(str(validated))` with no `fmt`. ezdxf default `fmt='dxf'` (ezdxf:345) vs COM `fmt='dwg'` (com:515) vs save_as tool default `'dwg'` (server:336) → 3-way inconsistency; format backend-dependent, not path-derived. | server.py:2299 | **Verified** (omits fmt) | +| R11 | **MED** | `drawing_save_as(fmt='dwg')` on ezdxf writes DXF bytes to a `.dwg` path and returns `{'format':'dwg'}` — silent wrong-format file. ezdxf cannot write DWG; should error or use `odafc`. | ezdxf_backend.py:345-352 | — | +| R12 | **MED** | `dimension_angular` ignores tx/ty, fixed `distance=10.0` → arc radius wrong for large/small parts; breaks COM parity. | ezdxf_backend.py:705-722 | — | +| R13 | **MED** | `entity_offset` ignores `side_x/side_y` (both backends); only `distance` sign flips side; ARC/LWPOLYLINE/ELLIPSE raise "not supported". | ezdxf:868-908; com:860-874 | — | +| R14 | **MED** | Engineering titleblock/validator symbols import-guarded to `None` (engineering/__init__.py:40-51); if module truly fails to import, tools raise opaque "NoneType not callable" instead of clear ToolError. | server.py:2266,2294 | — | +| R15 | **MED** | `system_about` static `tool_groups` catalog drifted: omits all 15 engineering+premium tools, misfiles `entity_delete_many`. Agents inspecting capabilities can't see the CLAUDE.md-mandated tools. | server.py:2070-2106 | — | +| R16 | **MED** | COM `transaction_active` left stale-True if commit/rollback `_run` raises; `rollback` issues `EndUndoMark`+`UNDO B` SendCommand without the CMDACTIVE guard used elsewhere → can interleave mid-command. | com_backend.py:1402,1411-1421 | — | +| R17 | **MED** | `_safe_send_command` busy-polls CMDACTIVE with `time.sleep(0.05)` on the single STA thread → monopolizes the executor up to deadline; `deadline_s`(8s) decoupled from `_run` timeout(60s). | com_backend.py:1521-1536 | — | +| R18 | **MED** | `drawing_new` swallows bootstrap failures into `result['bootstrap']={'error':...}` and reports success → un-bootstrapped drawing looks normal though CLAUDE.md treats the scaffold as a precondition for gear_*/titleblock_*. | server.py:280-294 | — | +| R19 | **MED** | `dimension_aligned`/`dimension_angular` permanently `xfail(strict=False)` — advertised tools shipped broken; an accidental fix wouldn't alert. | tests/test_dimensions.py:16-27 | — | +| R20 | LOW | `_registered_tool_count` reaches into FastMCP privates (`mcp._local_provider._components`, `'tool:'` prefix); a minor upgrade → tool_count=-1 surfaced to clients. | server.py:231-234 | — | +| R21 | LOW | Involute flank prepends a straight radial root→base segment (no trochoid fillet) → geometrically wrong dedendum for `root_r=base_r`; degenerate arc for teeth≤2. | engineering/gear.py:40-50,163-190 | — | +| R22 | LOW | `drawing_undo`/`transaction_rollback` share one `_undo_stack` with no transaction-id/depth separation; reload doesn't reset `_doc_path`/`_current_layer`. | ezdxf_backend.py:470-484,1462-1476 | — | +| R23 | LOW | COM `system_run_lisp` always reports `result='nil'` (SendCommand returns None); no CMDACTIVE guard → a prompting LISP form deadlocks next call. | com_backend.py:1487-1492 | — | +| R24 | LOW | `_apply_attrs` sets `entity.dxf.linetype` without `_ensure_linetype_loaded` (unlike set_properties/layer_modify) → `linetype='HIDDEN'` on create may render Continuous or raise on save. | ezdxf_backend.py:491-501 | — | +| R25 | LOW | `block_find_references` calls `entity_list(type_filter='INSERT')` with no limit (bypasses `max_list_limit`) → unbounded INSERTs into memory; assumes every backend populates `properties['block_name']`. | server.py:1362-1370 | — | +| R26 | LOW | View zoom ops unconditionally registered but no-op on ezdxf → success response implies the view changed when it didn't; `view_zoom_and_screenshot` may not frame the requested region on ezdxf. | server.py:1814-1831,1870 | — | +| R27 | LOW | `validator.py` title check substring-matches 'SPUR'/'HELICAL SPUR' on TEXT layer → false-positive blocks `drawing_finalize` if any note contains 'SPUR'. | engineering/validator.py:166-181 | — | +| R28 | LOW | COM `_find_autocad_hwnd` grabs first visible window whose title contains 'AutoCAD' → may capture palette/splash/About dialog; not tied to `AcadApplication.HWND`. | com_backend.py:283-300 | — | +| R29 | LOW | Broad `except Exception: pass` swallows COM faults (disconnected/busy) at debug level in ~20 sites → bypasses the HRESULT reconnect logic in `_run`. | com_backend.py:199,244,868,901,1216,… | — | + +--- + +## 5. Doc-vs-Code Drift Table + +| Claim (source) | Reality | Verdict | +|---|---|---| +| "87 tools" (CLAUDE.md, README:6) | **109** `@mcp.tool` (verified) | STALE (soft — both add "count is dynamic" caveat) | +| "104 tests" (README:83) | **184** test functions across 14 files (verified) | STALE (~80 under) | +| Section 1 "(11 tools)" (CLAUDE.md) | 12 (`drawing_redo` server.py:426) | WRONG | +| Section 9 "(5 tools)" (CLAUDE.md) | 4 view tools registered | WRONG | +| "12-section taxonomy" (CLAUDE.md) | 1–12 + 8b/8c/8d + 13 = 6 extra groups | INCOMPLETE | +| Premium workflow "non-negotiable for production" (CLAUDE.md) | 8/8 premium tools dead on COM (default Windows backend) | OVERCLAIM | +| "MIT — see [LICENSE](LICENSE)" (README:409) | No LICENSE file (verified absent) | **DEAD LINK / legal drift** | +| `benchmarks/` referenced (README:39,302,367) | Absent (verified) | DEAD LINK (forward-looking) | +| ".github/workflows/ci.yml" + "GitHub Actions CI" (README:84,358) | git status `D`; absent on disk (verified) | **CI REMOVED while docs claim it** | +| "Premium discipline lives in `.claude/skills/autocad-mcp-premium/`" (CLAUDE.md) | Entire tree git-deleted (28 deletions) + all `.cursor/` deleted | DANGLING POINTER | +| README tests/ tree (README:347-355) | Omits 6 of 14 test files + engineering/ package | STALE | +| `system_about` tool_groups | Omits 15 engineering+premium tools | DRIFT | +| `BlockInfo.description` field (base.py:71) | Never populated by either backend | DEAD FIELD | +| critique.py:53 "normalises to mm" comment | Stores raw DXF lineweight (ezdxf:189) | FALSE COMMENT | +| `engineering/__init__.py:37-39` "Agent B … in flight"; com_backend "Task #7/#8/#9" | Multi-agent build seams shipped unreconciled | TODO-IN-PROD | +| Dockerfile, .env.example (README) | Present (verified) | OK | + +--- + +## 6. Test Coverage Gaps (ranked) + +1. **COM backend: 0% coverage** (com_backend.py entire file) — transactions (UndoMark vs ezdxf snapshot are *different* semantics), SendCommand corner ops, CMDACTIVE guard, timeout/HRESULT recovery, all 8 premium stubs. Highest-risk untested surface; it's the production path. (R6) +2. **Server→sanitizer integration** — no test proves tools invoke `sanitize_command/lisp`/`validate_path`. (R7) +3. **Sanitizer adversarial cases** — LISP aliasing, command word-boundary evasion (`.ERASE`), UNC/`~`/symlink path escapes — the comment claims defenses with no tests. (R8) +4. **Critique detection paths** — `iso128`, `layer_color`, `dim_overlap` only exercised on a *clean* drawing (assert they *don't* fire); their actual detection logic and the unknown-focus branch (critique.py:241) are never proven. (R3, R4) +5. **`dimension_auto` ordinate** (ezdxf:1988) untested; chain/baseline covered. +6. **`dimension_aligned`/`angular`** permanently xfail — shipped broken. (R19) +7. **Uncovered backend methods (both backends):** `system_get/set_variable`, `system_status`, `drawing_close`, `drawing_export_pdf`, `entity_create_block_ref`, `block_set_attributes`, `linetype_list/load` (load-bearing for bootstrap), `view_zoom_*`, `view_screenshot`. +8. **`engineering/section.py`** (dead code) — 0 coverage. +9. **Transaction nesting/error semantics** — only single begin→commit/rollback tested; nested, rollback-without-begin, layer/block restoration untested. +10. **Infrastructure:** CI workflow deleted (no PR gate); `pytest-cov` declared but no `--cov`/`fail-under`; several "template/validation" tests assert nothing about the named feature (vacuous `if start:` guards, test_batch_template.py:30-34). + +--- + +## 7. Research Synthesis + +### 7a. Academic techniques (2025–2026) that validate or upgrade this design + +- **GeoGramBench (ICLR 2026, arxiv.org/abs/2505.17653):** frontier LLMs score **<50%** at high-abstraction Program-to-Geometry. → Empirically justifies the repo's no-coordinate-guessing rule and `point_from_snap`/`entity_select_smart`. **Action:** expand the deterministic geometry surface (`point_intersection`, `point_offset`, `point_along`) so the LLM *never* computes coordinates. +- **Self-Improving CAD w/ FEA feedback (arxiv.org/html/2605.17448v2):** deterministic controller owns validation, LLM owns design; **+13.4pp** per feedback round; frontier models get **zero** first-attempt strict passes — *iteration beats more tokens*. → This is the repo's architecture, but `drawing_critique` is one-shot. **Action:** make it a routed repair loop; `Issue` (plan_spec.py:64-81) already carries severity/handles/detail — add per-finding repair hints. +- **CADSmith (arxiv.org/abs/2603.26512):** 5-agent Planner/Coder/Executor/Validator/**Refiner**, dual loop, exact-kernel measurements + **independent VLM judge** → Chamfer 28.37mm→0.74mm (**38×**), IoU 0.81→0.96; *removing the rendered image alone made hard cases 35× worse.* → The repo renders a screenshot at finalize (server.py:2301-2309) but only checks bytes exist (validator step 7). **Highest-leverage single upgrade: feed that render to a VLM judge + add an automated Refiner.** +- **CADFusion (ICML 2025, arxiv.org/abs/2501.19054)** & **Seek-CAD (arxiv.org/abs/2505.17702):** render is a first-class signal; step-wise visual feedback catches errors mid-build. → Add a `visual` critique focus; run critique per construction step using the existing transaction model. +- **ProCAD "Clarify Before You Draw" (arxiv.org/html/2602.03045v1):** one-round clarification → invalidity 14.6%→0.9%, Chamfer ~80% lower. → `drawing_plan` records intent passively; add a pre-plan ambiguity/conflict detector that returns targeted questions before any `entity_create_*`. +- **FutureCAD (arxiv.org/abs/2603.11831):** NL → exact B-Rep primitive grounding fixes the fillet/chamfer "which primitive" problem. → Directly targets the repo's hardest API: trim/extend/fillet/chamfer need exact handles **plus** keep_x/keep_y. NL grounding ("fillet the top-left corner") removes the burden GeoGramBench shows LLMs fail at. +- **Text-to-CadQuery (2505.06507), CAD-Coder (2505.14646), EvoCAD (2510.11631):** field converged on Python/CadQuery macros; open local VLMs read CAD images reliably; candidate-generate + critique-as-fitness raises first-try quality. → Add more parametric meta-tools (the gear/keyway pattern) and/or a sandboxed parametric-script tool; an open VLM (CAD-Coder, **Florence-2 for GD&T**, arxiv.org/abs/2411.03707) keeps the visual tier off frontier APIs. +- **MUSE (arxiv.org/html/2605.28579) + CadBench (2605.10873):** grade manufacturability/functionality, not just shape; standard metrics = volumetric/surface IoU, Chamfer, **Invalidity Ratio**. → Replace `validator.py`'s regex/title heuristics with a staged code→geometric→rubric-judge protocol; emit a single scalar "drawing score" for regression tests (the repo has *no* quantitative quality metric). + +### 7b. Competitive landscape + +| Product | 3D | Render-critique loop | Validation gate | HTTP auth | Code escape hatch | +|---|---|---|---|---|---| +| **AutoCAD MCP Pro** | ❌ 2D only | ⚠️ finalize-only, symbolic | ✅ **unique** (critique+validator+ISO) | ❌ none | ⚠️ COM-only run_command/lisp | +| build123d-mcp/agentcad (github.com/pzfreo/build123d-mcp) | ✅ | ✅ in-loop | ❌ | — | ✅ | +| FreeCAD Robust MCP (150+ tools) | ✅ + FEM | ⚠️ | ❌ | — | ✅ | +| Blender MCP (~22k★) | ✅ | ✅ | ❌ | — | ✅ Python exec | +| RhinoMCP | ✅ | ✅ viewport | ❌ | — | ✅ | +| Fusion360/Onshape MCP | ✅ parametric | — | ❌ | ✅ OAuth/keys | — | + +Snyk's 9-CAD-MCP roundup (snyk.io/articles/9-mcp-servers-for-computer-aided-drafting-cad-with-ai/): **vision only in FreeCAD/Blender/Rhino; design validation in none; HTTP auth only in Onshape/Fusion.** → **Our moat is the design-rule validation gate; the field beat us on 3D, in-loop render-critique, and auth.** Autodesk's own in-AutoCAD assistant demo (aecmag.com/features/autodesk-shows-its-ai-hand/) flagship = **automated compliance checking** — exactly our niche, so we must make standards compliance the sharpest, most-cited capability. Adam/Zoo (techcrunch.com/2025/10/31/...) set the UX bar: **parametric editability + post-gen dimension sliders + entity-level conversational editing**. + +### 7c. Under-used COM / ezdxf API + ISO standards + +**COM (ActiveX) gaps (every create call hits ModelSpace, com_backend.py:125-127):** +- **PaperSpace/Layouts + Plot config** — `drawing_export_pdf` (com_backend.py:528-534) calls bare `Plot.PlotToFile(path,'DWG To PDF.pc3')` with no page setup → ignores the A3/scale `drawing_plan` promised. Add `layout_*`/`viewport_*` (AddPViewport, StandardScale, CanonicalMediaName). (help.autodesk.com ActiveX ref) +- **AddTable** (BOM/revision tables), **AddMLeader** (ISO leader notes — CLAUDE.md *reserves* "leader notes" but no tool creates them), **Fields** (auto-updating title block), **GetDynamicBlockProperties** (parametric symbol libraries — `block_insert` only does attribute text, com:1195-1251), **DimStyles.Add+CopyFrom** (no ISO-129 dimstyle is ever set → undercuts the "Dimensions are ISO 129" rule), **named UCS** (per-view frames for `view_count`), **AttachExternalReference** (shared borders), **gradient/island hatch**. +- **3D solids/regions/booleans** — `system_status` advertises `all_entity_types` (com:1443) which is **false**; either implement `solid_*` or correct the claim. +- **Push selection into AutoCAD:** `entity_list` (com:967) and `analysis_select_by_type` (com:1352) loop ModelSpace.Item(i) in Python (O(n) COM round-trips — the #1 perf killer); `analysis_select_by_layer` (com:1339) already shows the right `SelectionSet.SelectAll` group-code-filter pattern to copy. + +**ezdxf gaps (Grep confirms zero use of `set_gradient/set_solid_fill/paperspace/add_viewport/odafc/export_dwg/groupby/dimstyles/MTextEditor/add_ordinate/qsave`):** +- **`addons.odafc.export_dwg`** = real DWG (R12–R2018) → fixes R11/R10 (drawing_save_as fake DWG). (ezdxf.mozman.at/docs/addons/odafc.html) +- **drawing-addon `Configuration`** (lineweight_policy ABSOLUTE, background_policy, SVGBackend/PyMuPdfBackend) → true ISO lineweights + vector output, replacing raw matplotlib (ezdxf:357-379,1400-1424). +- **`query()` mini-language + `groupby()`** → replace hand-rolled loops in analysis_stats/select/entity_select_smart (which pulls 5000 entities then filters in memory, ezdxf:2008-2050). +- **`recover.readfile` + `Auditor.fix`** → real repair; `drawing_audit` (ezdxf:448) reports but never fixes; `drawing_open` (ezdxf:309) has no recover fallback. +- **`MTextEditor`** (stacked tolerance fractions), **Hatch gradients/islands/edge-paths**, **DimStyle overrides** (no ISO dimstyle today, ezdxf:668-774), **path-module offset** (generalizes LINE/CIRCLE-only offset + LINE-only corner ops). + +**ISO standards conformance:** +- ✅ **ISO 128-2** lineweight set encoded (plan_spec.py:85, verified) — but **nulled by R2** and group-vs-element-class not checked. +- ⚠️ **ISO 7200** title block — A3-only hardcoded (titleblock.py:16-17), informal labels ("DWG NO" vs ISO field names), len×0.3 centering heuristic (titleblock.py:171), missing "Document type"/"Date of issue". +- ❌ **ISO 129-1 dimensioning** — biggest functional gap: `dimension_*` (base.py:226-257) have **no** param for ±/limit/fit (H7/g6) tolerances or text override → toleranced production dims impossible. (iso.org/obp/ui/#iso:std:iso:129) +- ❌ **ISO 1101 / ASME Y14.5 GD&T** — entirely absent (grep-confirmed): no feature control frame, datum, or ISO 2768 general-tolerance note. (gdandtbasics.com/iso-vs-asme-standards/) +- ⚠️ **ISO 13567** — static mech-only starter list (layers.py:51-62), no field-grammar parser/validator. + +--- + +## 8. "What Would Make This the BEST AutoCAD MCP" — Prioritized Thesis + +The evidence converges: **the repo is architecturally ahead on dual-backend + standards-validation but behind on the closed-loop, the production-output plumbing, and the very workflow it markets.** Priorities, evidence-backed: + +**P0 — Make the marketed product actually work on its default platform.** +1. **Implement the 8 premium meta-tools on COM** (R1, com_backend.py:1637-1660). Today the CLAUDE.md-mandatory workflow 500s on every Windows install. Nothing else matters until this ships. +2. **Fix the lineweight `int()` truncation** (R2, ezdxf_backend.py:1034 → multiply mm by 100 / map to ACAD enum). This single bug nullifies the ISO-128 system *and* makes the iso128 critique a permanent no-op (R3). The standards-validation gate is the **competitive moat** (Snyk roundup; Autodesk's own flagship demo) — it must be real. +3. **Add CI + a coverage gate + the first COM smoke tests** (R6, ci.yml deleted; pytest-cov unconfigured). Restore the LICENSE file (legal drift). + +**P1 — Own the moat by closing the loop (the #1 accuracy lever in every 2025–26 paper).** +4. **Turn `drawing_critique` into a routed iterative Refiner loop** with per-`Issue` repair hints and a bounded budget (CADSmith Refiner; FEA-agent +13.4pp/round; Physics-in-the-Loop 3.3–6.0 iters converge). The `Issue` dataclass already carries handles+detail. +5. **Promote the finalize screenshot to a first-class `visual` critique focus fed to a VLM judge** (CADSmith: dropping the render → 35× worse; CADFusion). Use an **open local VLM** (CAD-Coder / Florence-2 for GD&T) behind a `[vision]` extra so the headless path is unaffected. Add a **dimension round-trip check** (VLM-read dims vs created `dimension_*` entities) to catch what proximity-only `dim_overlap` (R4) cannot. +6. **Add a ProCAD-style pre-plan clarification pass to `drawing_plan`** (invalidity 14.6%→0.9%): detect missing/conflicting dims, material, tolerance, view_count-vs-intent before any geometry. +7. **Emit a scalar "drawing score" / Invalidity-Ratio** (MUSE/CadBench) so the premium workflow is regression-testable — the repo has no quantitative accuracy metric. + +**P2 — Reach production-drawing parity (what un-toleranced ISO 128 can't deliver).** +8. **ISO 129 tolerances + ISO 1101/Y14.5 GD&T**: add ±/limit/fit params to `dimension_*` and a `gd_frame(symbol, tol, datums, modifiers)` + datum/ISO-2768-note primitive. This is the categorical gap blocking real part drawings; no surveyed text-to-CAD product covers 2D GD&T — a differentiator. +9. **Real plotted sheets**: COM Layout/PaperSpace/Plot-config so `drawing_plan(sheet_size, scale)` and `titleblock_apply_iso_a3` produce a correct A-series plot; on ezdxf wire `odafc.export_dwg` (kills the fake-DWG R11) and the drawing-addon `Configuration` for true-lineweight SVG/PDF. +10. **An ISO-129 `DimStyle`** created once and applied to all dims (COM `DimStyles.Add+CopyFrom`; ezdxf dimstyle overrides) so "Dimensions are ISO 129" becomes true, and **MLEADER** for the leader notes the docs already reserve. + +**P3 — Catch the field on breadth (table-stakes elsewhere, absent here).** +11. **A 3D solids tier** (extrude/revolve from 2D profiles + booleans on COM; STEP/STL on ezdxf) — *or* correct the false `all_entity_types` claim (com:1443). Every competitor has 3D. +12. **Parametric editability + entity-level conversational editing** (Adam/Zoo): capture driving dims in PlanSpec, expose a re-drive tool; "select this circle → make it the 20mm bore" via existing handle model + `entity_select_smart`/`point_from_snap`. +13. **Authenticated remote HTTP** (API-key middleware) given `dangerous_commands_enabled` exists (server.py:111); a **standard-parts/symbol library** (`library_insert` over `block_insert`, dynamic-block props); **NL→B-Rep grounding** (FutureCAD) over the brittle trim/fillet/chamfer keep-point API. + +**P4 — Hygiene that compounds with the above.** Generalize LINE-only corner ops + LINE/CIRCLE-only offset via the ezdxf path module; replace hand-rolled selection with `query()`/`groupby()` (ezdxf) and `SelectionSet.SelectAll` filters (COM); fix `entity_array_polar` 360° dup (R9), `dimension_angular` tx/ty (R12), the COM thread-leak `CoUninitialize` (R5); delete dead `section.py`/`generate_tooth_profile`; reconcile `system_about` catalog (R15) and the stale README/CLAUDE.md drift (§5). + +**Files central to every priority:** `backends/com_backend.py:1637-1660` (P0.1), `backends/ezdxf_backend.py:1034` + `engineering/layers.py:14-26` (P0.2), `engineering/critique.py:37-64,194-197` (P0.2/P1.5), `engineering/plan_spec.py:64-87` (P1.4/P1.6/P2.8), `backends/base.py:226-257` (P2.8), `server.py:528-534/2294-2309` (P2.9). \ No newline at end of file diff --git a/docs/analysis/01-DIAGNOSTICS.md b/docs/analysis/01-DIAGNOSTICS.md new file mode 100644 index 0000000..9356848 --- /dev/null +++ b/docs/analysis/01-DIAGNOSTICS.md @@ -0,0 +1,309 @@ + + +# DIAGNOSTICS Report — AutoCAD MCP Pro + +Synthesized from the verified recon risk register + fresh-sweep findings. REFUTED / ALREADY-FIXED items are excluded from the ranked body and listed in the appendix. Duplicates between the two sources are merged. Line numbers reflect **current** code (verified against the working tree). + +--- + +## CRITICAL +_None._ + +--- + +## HIGH + +### N1 — COM `entity_create_hatch` builds an ASSOCIATIVE hatch then deletes its boundary polyline +- **Severity:** HIGH +- **Location:** `backends/com_backend.py:659-673` +- **Description:** `mspace.AddHatch(0, pattern, True)` requests an *associative* hatch linked to the appended boundary loop, but the code then calls `bnd_pline.Delete()` (`com_backend.py:673`) on that very polyline. In live AutoCAD this either errors on `Delete`, drops the boundary, or leaves a hatch whose boundary tracking breaks on the next regen/edit. +- **Fix:** Change the 3rd arg to non-associative — `AddHatch(0, pattern, False)` — so the computed fill survives deleting the throwaway boundary. If associativity is truly wanted, keep the boundary polyline instead of deleting it. + +### N2 — `drawing_save_as` default format diverges (ezdxf `"dxf"` vs COM/ABC `"dwg"`); `drawing_finalize` relies on the default +- **Severity:** HIGH (merges register **R10**/**R11** with the fresh-sweep duplicate) +- **Location:** ABC default `backends/base.py:158` (`"dwg"`); ezdxf `backends/ezdxf_backend.py:346` (`"dxf"`); COM `backends/com_backend.py:516` (`"dwg"`); consumed with no fmt at `server.py:2299` (`drawing_finalize` → `backend.drawing_save_as(str(validated))`). +- **Description:** `drawing_finalize` passes no `fmt`, so each backend's default decides the output. Two distinct real defects layer here: + 1. **ezdxf silently mislabels DWG (R11):** `drawing_save_as` *ignores* `fmt` entirely — `doc.saveas(path)` derives format from the extension — yet still returns `{"format": fmt}`. For `fmt="dwg"`/`"dwt"` it writes DXF tag-stream bytes (ezdxf has no DWG/DWT writer) while echoing `format:"dwg"`. (`ezdxf_backend.py:346-353`) + 2. **COM writes DWG content under a `.dxf` name (R10):** COM's `SaveAs(path, fmt_map[fmt])` honors `fmt`, *not* the extension, so a `.dxf` save_path under COM yields DWG bytes in a `.dxf`-named file. + - Net: identical `drawing_finalize(save_path=...)` produces different on-disk formats per backend, decoupled from the extension the user supplied. +- **Fix:** In `drawing_finalize`, derive `fmt` from the `save_path` extension and pass it explicitly. Unify the ABC + both impl signatures to one default. In ezdxf, reject `fmt not in {dxf, dwt}` (raise `ToolError` for `dwg`) instead of writing DXF and echoing `format=fmt`. + +--- + +## MED + +### R4 — `dim_overlap` critique focus is a guaranteed silent no-op (no backend populates the keys it reads) +- **Severity:** MED (claimed HIGH; downgraded — silently returns `[]`, no incorrect output) +- **Location:** reads `engineering/critique.py:199-201`; extractors `backends/ezdxf_backend.py:167-169` and `backends/com_backend.py:204-244` (verified at `critique.py:199-201`). +- **Description:** `_check_dim_overlap` reads `text_position`/`defpoint`/`insertion_point`, but the ezdxf DIM branch sets only `props["dim_type"]` and COM dims get only a generic `bounding_box`. None of the three keys is produced anywhere, so `ip` is always falsy, `dim_points` stays empty, and the focus always returns `[]`. +- **Fix:** Populate a dim reference point (text midpoint / defpoint) in both `_entity_info` extractors for DIM types, OR fall back to `properties["bounding_box"]` center in `_check_dim_overlap`. + +### R9 — ezdxf `entity_array_polar` overlaps the original on full-circle fill and diverges from COM +- **Severity:** MED +- **Location:** `backends/ezdxf_backend.py:947-948` +- **Description:** `step = radians(fill_angle) / max(count-1, 1)` with loop `range(1, count)` places the last copy at `step*(count-1) = 360°` for a full ring — coincident with the original at 0°. Also diverges from COM (`com_backend.py:921` uses AutoCAD `ArrayPolar`, spacing `360/count`): `count=4, fill=360` → COM 90/180/270 vs ezdxf 120/240/360. `count-1` is only correct for partial-arc fills. +- **Fix:** `divisor = count if abs(fill_angle % 360) < eps else max(count-1, 1)`. + +### R12 — ezdxf `dimension_angular` ignores `tx/ty` and hardcodes `distance=10.0` +- **Severity:** MED +- **Location:** `backends/ezdxf_backend.py:706-723` (hardcode at `:715`) +- **Description:** Accepts `tx/ty` (advertised as "text position X/Y" at `server.py:721-722`) but calls `add_angular_dim_2l(..., distance=10.0)`, ignoring them. COM honors `tx/ty` via `AddDimAngular` (`com_backend.py:770-784`), so identical inputs place the arc differently per backend; the fixed 10.0 mislocates the arc for very large/small parts. +- **Fix:** Pass `location=(tx,ty)` to `add_angular_dim_2l`, or compute `distance=hypot(tx-vx, ty-vy)`. + +### R13 / N2-offset — `entity_offset` ignores `side_x/side_y` in BOTH backends; side semantics diverge; ezdxf supports only LINE/CIRCLE +- **Severity:** MED (merges register **R13** with the two fresh-sweep offset findings) +- **Location:** ezdxf `backends/ezdxf_backend.py:869-909`; COM `backends/com_backend.py:861-875`; ABC sig `backends/base.py:315-319`; tool `server.py:859-868`. +- **Description:** Both backends accept `side_x/side_y` and never read them (dead params; the tool advertises them as "a point on the offset side"). ezdxf hard-fixes the LINE normal `Vec2(-dir.y, dir.x)*d` (`:882`) and CIRCLE `radius+d` (`:896`); every other type raises `RuntimeError("Offset not supported …")` (`:908`) — ARC/LWPOLYLINE/POLYLINE/ELLIPSE/SPLINE unsupported, contradicting the tool's "line, circle, or polyline" claim. COM delegates to `ent.Offset(distance)`, whose sign/side convention is not guaranteed to match ezdxf's fixed left-normal — same `(handle, distance)` can offset to opposite sides per backend. +- **Fix:** Honor `side_x/side_y` in both backends (pick the copy whose ref point is nearest the side point, or sign `distance` from the cross product against the offset direction). In ezdxf, support ARC/LWPOLYLINE/POLYLINE/ELLIPSE or drop the documented polyline/side claims. Reconcile so positive distance means the same side in both engines. + +### R15 — `system_about` `tool_groups` static dict omits ~20 registered tools and misfiles one +- **Severity:** MED +- **Location:** `server.py:2070-2106` +- **Description:** The hand-maintained `tool_groups` dict omits `drawing_close`, `entity_trim/extend/fillet/chamfer`, all 7 engineering tools, and all 8 premium tools (~15+ shown), and lists `entity_delete_many` under `entity_creation` though it's tagged `{entity, modify}` with `destructiveHint=True`. Live `total_tools` stays correct; only the per-group breakdown drifts. +- **Fix:** Derive `tool_groups` dynamically from registered tools' tags, or at minimum add the engineering/premium groups, move `entity_delete_many`, and add the 5 missing modify/drawing tools. + +### R18 — `drawing_new` bootstrap failures return as success +- **Severity:** MED +- **Location:** `server.py:282-293`; per-item swallow at `engineering/layers.py:90-91, 128-129` +- **Description:** Bootstrap is wrapped in `try/except` that stores `{"error": …}` and still `return result` representing a created drawing; per-item failures are caught internally as `"failed: {exc}"` strings. A fully- or partially-failed bootstrap looks like success, so geometry can land on missing engineering layers — a documented precondition violation. +- **Fix:** Surface `bootstrap_ok=False` / `status="degraded"` and/or raise `ToolError` when `bootstrap=True` and any linetype/layer reports `failed` or the catch-all fires. + +### R19 — ezdxf `dimension_aligned` / `dimension_angular` ship broken (xfail strict=False masks fix/regression) +- **Severity:** MED +- **Location:** markers `tests/test_dimensions.py:16, 23`; root cause `backends/ezdxf_backend.py:696` (`dist=`) and `:711-715` (`center=/p1=/p2=/distance=`) +- **Description:** ezdxf 1.4.3 `add_aligned_dim()` is `(p1, p2, distance, …)` but code passes `dist=`; `add_angular_dim_2l()` is `(base, line1, line2, …)` but code passes `center=/p1=/p2=/distance=` → `TypeError`. Both are advertised MCP tools (`server.py:695, :714`) and recommended in prompt text (`:2639`). `xfail(strict=False)` means an accidental fix XPASSes silently. +- **Fix:** `dist→distance`; `base=(vx,vy)`, `line1=((vx,vy),(x1,y1))`, `line2=((vx,vy),(x2,y2))`. Change markers to `strict=True` (or remove) so the suite alerts. + +### R22 — ezdxf transaction snapshots share the single `_undo_stack` with undo history; reload leaves `_current_layer` stale +- **Severity:** MED +- **Location:** `backends/ezdxf_backend.py:213` (single stack), `drawing_undo` `471-485`, `transaction_begin/commit/rollback` `1429-1477`, `system_status` `1489` +- **Description:** One `_undo_stack` is pushed/popped by both undo and transactions, with no txn id/marker/depth isolation; `system_status` reports `len(_undo_stack)` *as* `transaction_depth`. A `drawing_undo` silently consumes an open transaction's snapshot. On reload, undo/rollback re-read the doc but never reset `_current_layer`, so `is_current` reporting goes stale vs the snapshot's `$CLAYER`. +- **Fix:** Use a dedicated transaction list (or begin-markers/ids) distinct from undo history; on reload re-read `$CLAYER` into `self._current_layer`. + +### R5 — COM thread/apartment leak on `SendCommand` timeout (no `CoUninitialize`) +- **Severity:** MED +- **Location:** `backends/com_backend.py:416-434` (timeout path), `69-72` (`_com_init`), `387-391` (disconnect) +- **Description:** `_com_init` calls `CoInitialize()` as the executor initializer; `CoUninitialize` appears nowhere in source. On `TimeoutError`, `stuck.shutdown(wait=False)` cannot cancel a worker still blocked in `SendCommand` (the inline comment admits "Abandon it"), and a fresh executor is built — each timeout permanently leaks one `CoInitialize`'d thread + the COM apartment it pinned. `disconnect()` has the same pattern. +- **Fix:** Bound the leak (track abandoned executors, avoid unbounded re-creation); add per-thread `CoUninitialize` via worker-side `try/finally`/thread-local teardown so apartments release when a timed-out thread eventually unblocks. + +### R6 — COM backend (1651 lines, 85 async methods) has zero test coverage +- **Severity:** MED +- **Location:** `tests/conftest.py:5` (only `EzdxfBackend` imported); untested logic incl. CMDACTIVE guard `com_backend.py:1474`, transactions `1397-1419` +- **Description:** No test references `ComBackend`/`win32`/`SendCommand`/`CMDACTIVE`. The `backend` fixture instantiates only ezdxf. SendCommand-driven commands, the CMDACTIVE busy-guard + poll loop, and transaction begin/commit/rollback are unexercised, so COM logic can regress silently on non-Windows CI. +- **Fix:** Add `tests/test_com_backend.py` unit-testing the mockable logic (CMDACTIVE guard, `normalize_lineweight` usage, `_safe_send_command` polling, value coercion) with a mocked COM `app`/`doc`, gated by `@pytest.mark.skipif` when `win32com` is unavailable. + +### N3 — `entity_select_smart` `length_range` always rejects ARCs (no backend populates ARC `"length"`) +- **Severity:** MED +- **Location:** `backends/base.py:897-904` +- **Description:** The gate fires for `("LINE","ARC")` and reads `e.properties.get("length")`, but the ARC extractor branches (`ezdxf_backend.py:135-139`, `com_backend.py:217-222`) set only center/radius/angles — never `length`. So every ARC has `length is None → return False`, silently excluding all arcs despite the advertised ARC support. +- **Fix:** Populate `props["length"]` for ARC (= `radius * |Δangle|` in radians, with wraparound) in both extractors, OR compute arc length on the fly in `entity_select_smart`, OR drop "ARC" from the gate and docstring. + +### N4 — `dimension_auto` hardcodes `layer="DIM"`, which does not exist in the `iso13567` layer set +- **Severity:** MED +- **Location:** `backends/base.py:822, 836, 845, 852` +- **Description:** Every `dimension_linear` inside `dimension_auto` passes `layer="DIM"`, but the iso13567 set's dim layer is `"M-DIMEN-T-N"` (`engineering/layers.py:57`). After `drawing_apply_iso_layers("iso13567")`, dims land on an auto-created `"DIM"` (default color 7 / lw -3), bypassing the ISO layer. `_check_layer_color` won't flag it because `_EXPECTED_LAYER_COLOR` is built only from `ENGINEERING_LAYERS`. (The hardcoded `"CONSTRUCTION"` in `construction_xline/clear`, `base.py:743/:754`, has the same iso13567 mismatch — its construction layer is `"M-CONST-E-N"`.) +- **Fix:** Resolve the dimension (and construction) layer from the active layer set via a registry lookup keyed on `PlanSpec.layer_set_id` (mech→DIM, pid→DIM, iso13567→M-DIMEN-T-N) instead of literals. + +### N5 — `EntityInfo.properties` shape diverges by backend (`bounding_box`, TEXT `rotation`, MTEXT `char_height`) +- **Severity:** MED (merges three fresh-sweep property-parity findings) +- **Location:** `bounding_box`: `com_backend.py:192-199` (every entity) vs `ezdxf_backend.py:122` (never); TEXT/MTEXT: `ezdxf_backend.py:144-152` vs `com_backend.py:229-237`. +- **Description:** COM seeds `properties["bounding_box"]` for *every* entity; ezdxf never adds it. ezdxf TEXT sets `rotation` and MTEXT sets `char_height`; COM TEXT/MTEXT set neither (COM MTEXT sets no height at all). Any client reading these keys works on one backend and `KeyError`s on the other. +- **Fix:** Pick a contract and enforce it both ways: add `bounding_box` to ezdxf via `ezdxf.bbox.extents` (or drop it from COM and expose extents only via `analysis_bounding_box`); add `rotation`/`char_height` to the COM TEXT/MTEXT branches (`rad2deg(entity.Rotation)`, `entity.Height`). Document the props contract. + +### N6 — COM `system_run_lisp` always reports `result:"nil"` (SendCommand is void) +- **Severity:** MED (duplicates register **R23**'s value-capture half; R23's no-CMDACTIVE-guard half tracked at LOW below) +- **Location:** `backends/com_backend.py:1488-1493` +- **Description:** `result = doc.SendCommand("(progn …)")` then `str(result) if result else "nil"`. AutoCAD COM `SendCommand` is void-returning → `result` is always `None` → the tool unconditionally returns `"nil"` regardless of the actual LISP value. +- **Fix:** Drop the bogus `result` field, or capture out-of-band: `(setvar "USERS1" (vl-princ-to-string ))` then `GetVariable("USERS1")` after completion. + +--- + +## LOW + +### R10/R11 residual — _covered under N2 (HIGH)._ No separate LOW entry; the format-default divergence is fully captured there. + +### R16 — COM `transaction_commit/rollback` leave `_transaction_active` stale on error; rollback's `UNDO B` bypasses the CMDACTIVE guard +- **Severity:** LOW +- **Location:** `backends/com_backend.py:1406-1423` (commit `1406-1413`, rollback `1415-1423`; begin mirror at `1402-1403`) +- **Description:** `_transaction_active` is set/cleared *after* `await self._run(_sync)` with no `try/finally`; if `_run` raises, the flag stays stale. Rollback's `_sync` issues raw `doc.SendCommand("_UNDO B\n")` (`:1419`) without the CMDACTIVE pre-check/poll that `system_run_command`/`_safe_send_command` use — the documented deadlock hazard. +- **Fix:** Clear `_transaction_active` in a `finally` around `_run`; route rollback's `UNDO B` through `_safe_send_command`. + +### R20 — `_registered_tool_count()` reaches through FastMCP private attrs; fails to `-1` and surfaces it +- **Severity:** LOW +- **Location:** `server.py:225-234` (private access at `:231-232`) +- **Description:** `mcp._local_provider._components` + the undocumented `"tool:"` key prefix, with `except Exception: return -1`. `-1` is surfaced unguarded as `tool_count` (`server.py:1969/1976/1983`) and `total_tools` (`:2107`). A minor `fastmcp>=3.0` restructuring silently degrades the count to `-1`. +- **Fix:** Use the public API (e.g. `len(await mcp.get_tools())`), making the helper async. + +### R23 — COM `system_run_lisp` has no CMDACTIVE guard (deadlock on prompting LISP) +- **Severity:** LOW (value-capture half merged into N6) +- **Location:** `backends/com_backend.py:1488-1493` +- **Description:** Unlike `system_run_command` (guards at `1473-1482`) and `_safe_send_command` (polls at `1521-1537`), `system_run_lisp` sends raw with no CMDACTIVE pre-check/poll. A prompting form (`(command "_LINE")`, `(getpoint)`) leaves CMDACTIVE non-zero and deadlocks the next COM call. +- **Fix:** Route through `_safe_send_command` (or add the same pre-send guard + post-send poll). + +### R24 — ezdxf `_apply_attrs` assigns linetype without `_ensure_linetype_loaded` → undefined names silently render Continuous +- **Severity:** LOW +- **Location:** `backends/ezdxf_backend.py:501-502` +- **Description:** `_apply_attrs` does `entity.dxf.linetype = linetype` with no load check, unlike `entity_set_properties:981`, `layer_create:1030`, `layer_modify:1075`. `entity_create_line` forwards a documented `linetype` param (`server.py:446`; a prompt even says "use entity_create_line with CENTER linetype"), but `_BUILTIN_LINETYPES` is only `{continuous, bylayer, byblock}`, so HIDDEN/CENTER fall through to Continuous. +- **Fix:** Call `_ensure_linetype_loaded(self._require_doc(), linetype)` before the assignment. + +### R26 — ezdxf `view_zoom_window` is a no-op but reports success; `view_zoom_and_screenshot` ignores the window region +- **Severity:** LOW +- **Location:** `backends/ezdxf_backend.py:1395-1399` (zoom no-ops), `1401-1425` (screenshot ignores window); `server.py:1887-1893` +- **Description:** ezdxf zoom ops return `{"ok": True, "message": …}` while doing nothing. `view_zoom_and_screenshot` passes window coords to the no-op, then `view_screenshot` renders the entire modelspace via `draw_layout(msp)` with `bbox_inches="tight"`, ignoring `x1/y1/x2/y2` — so the image frames the whole drawing despite the docstring promising a window. +- **Fix:** Store a pending crop region in `view_zoom_window` and apply via `ax.set_xlim/set_ylim` (drop `bbox_inches="tight"` when cropping); or return `ok:False` for ezdxf window zoom so success no longer implies framing. + +### R28 — COM screenshot HWND via title-substring `EnumWindows` instead of `AcadApplication.HWND` +- **Severity:** LOW +- **Location:** `backends/com_backend.py:284-301` (`:292`) +- **Description:** `_find_autocad_hwnd` picks the first visible top-level window whose caption contains `"AutoCAD"` — a palette, About dialog, or Start tab can win (EnumWindows ~ top-Z first). The COM app object's `.HWND` (the real main frame) is unused. +- **Fix:** Use `int(_acad_app().HWND)`; keep EnumWindows only as a fallback. + +### R21 — Gear flank/root junction is a straight radial segment, not a fillet; radial discontinuity when `root_r >= base_r` +- **Severity:** LOW +- **Location:** `engineering/gear.py:40-50` (flank), `163-190` (root-arc stitch) +- **Description:** `generate_involute_flank` prepends `(root_r, 0.0)` while the involute starts at `(base_r, 0.0)` — both on y=0, so root→base is a straight radial segment, not a trochoid. When `root_r >= base_r` (z≥42 @20°, z≥27 @25°, all within the teeth≥6 API limit) no root point is added but the root arc is drawn at `root_r`, giving a radial jump in the closed polyline (verified m=2,z=60: 1.118 mm). (The teeth≤2 degenerate case is unreachable — tools enforce teeth≥6.) +- **Fix:** Anchor the flank/root junction at `root_r` (radially extend the first involute point, or use a true trochoid below the base circle); skip the root arc when `root_r >= base_r`. + +### N7 — `point_from_snap` reads ARC angles via bracket indexing → opaque `KeyError` instead of the function's standard `RuntimeError` +- **Severity:** LOW +- **Location:** `backends/base.py:629-630, 646-647` +- **Description:** ARC `"end"`/`"mid"` branches use `p["start_angle"]`/`p["end_angle"]` direct subscripting, while every other geometry access uses `p.get(...)` + descriptive `RuntimeError`. If a backend's extraction failed (broad try/except swallows at `ezdxf:170`/`com:245`), this raises an opaque `KeyError` rather than the consistent error. +- **Fix:** Use `p.get(...)` with the same None-check `RuntimeError` pattern as `_line_pts`/`_circle_geom`. + +### N8 — PlanSpec stored in `_plan_spec` is never read; `drawing_critique` does not "replay" it as documented +- **Severity:** LOW +- **Location:** write `backends/base.py:574`; never read in `engineering/critique.py` `run_critique` (`234-251`); docstrings `base.py:563`, `server.py:2351` +- **Description:** `drawing_plan` stores `self._plan_spec = plan`; docstrings claim it's "held by the backend and replayed by drawing_critique." But `run_critique` and all `_check_*` never reference `_plan_spec`/`.intent`/`.scale`/`.view_count`/`.dim_style` (grep: written once, read nowhere). The committed plan is dead state and the "replay" contract is unmet. +- **Fix:** Implement plan-vs-drawing checks that read `backend._plan_spec` (e.g. view_count vs detected views, dim_style vs dims present), or correct the docstrings to stop claiming replay. + +### N9 — `view_zoom_extents`/`view_zoom_window` return different payload shapes (ezdxf adds `message`, COM omits it) +- **Severity:** LOW +- **Location:** `backends/ezdxf_backend.py:1395-1399` vs `backends/com_backend.py:1372-1385` +- **Description:** ezdxf returns `{"ok": True, "message": …}`; COM returns `{"ok": True}` with no `message`. Same MCP tool, two shapes — a client keying on `result["message"]` works on ezdxf and `KeyError`s/None on COM. +- **Fix:** Return identical keys from both backends (always include `message` + an `applied` bool). + +--- + +## SMELL + +### R17 — COM `_safe_send_command` polling holds the lone STA thread; inner `deadline_s` decoupled from outer `com_call_timeout` +- **Severity:** SMELL (downgraded from MED — functionally correct; inner 8s deadline is tighter than the 60s outer net, 50 ms sleep is a yielding wait, and holding the STA thread is unavoidable under STA) +- **Location:** `backends/com_backend.py:1498-1542`; `config.py:38` +- **Fix (optional):** Tie `deadline_s = min(8, com_call_timeout*0.8)` so the two stay consistent if the env var changes. + +### R29 — Broad `except Exception` guards around optional COM reads +- **Severity:** SMELL (downgraded — adjacent unguarded calls re-raise genuine disconnects into `_run`; most sites log rather than silently pass; reconnect is not bypassed) +- **Location:** `backends/com_backend.py:200, 245, 251, 470, 1447, 1512-1548` (~30 sites) +- **Fix:** Narrow to `except (pywintypes.com_error, AttributeError)` so genuine logic bugs surface. + +### S5 — Two parallel, conflicting layer systems +- **Severity:** SMELL +- **Location:** `server.py:1647-1688` (`_LAYER_TEMPLATES`) vs `engineering/layers.py:14-69` (`ENGINEERING_LAYERS`/`LAYER_SET_REGISTRY`) +- **Description:** `_LAYER_TEMPLATES` (architectural/mechanical/…) uses hundredths-int lineweights, all-Continuous linetypes, no CONSTRUCTION layer; `engineering/layers.py` (mech/pid/iso13567) uses mm-floats, real HIDDEN/CENTER, ISO-128 lineweights, CONSTRUCTION. The overlapping mechanical/mech sets *conflict* (HIDDEN/CENTER colors swapped), and `_LAYER_TEMPLATES` violates the ISO-128 discipline `drawing_critique` enforces. +- **Fix:** Retire `_LAYER_TEMPLATES`/`template_apply_layers` in favor of `LAYER_SET_REGISTRY` (or map legacy keys onto it) for one source of truth. + +### S3 — `BlockInfo.description` always empty +- **Severity:** SMELL +- **Location:** declared `backends/base.py:71`; never set in `ezdxf_backend.py:1196-1202` / `com_backend.py:1186-1192` +- **Fix:** Populate `description=blk.block.dxf.get("description", "")` (ezdxf) + COM equivalent, or drop the field. + +### S1 — `engineering/section.py` helpers (`cutting_plane_line`, `apply_section_hatch`) are dead +- **Severity:** SMELL +- **Location:** `engineering/section.py:12, 65`; only re-exported (`__init__.py:35, 65-66`), never called (`gear_draw_section_aa` builds its hatch inline). +- **Fix:** Wire a `section_*` MCP tool around them, or delete the module + exports. + +### S2 — `generate_tooth_profile` is dead (superseded by `generate_full_gear_outline`) +- **Severity:** SMELL +- **Location:** `engineering/gear.py:74`; exported `__init__.py:9, 59`; duplicates ~80% of the outline math (unused local `t_pitch` at `:94`). +- **Fix:** Delete and remove import/`__all__` entries, or wire into a draw path. + +### S4 — Stale "Agent B still in flight" build-seam comment + now-vestigial import guards +- **Severity:** SMELL +- **Location:** `engineering/__init__.py:37-39` (comment), `40-51` (try/except guards) +- **Description:** Both `titleblock.py` and `validator.py` now exist and import cleanly, so the guard's rationale is stale. (Related: **R14** — when a guarded symbol *is* None the tools call it and raise an opaque `TypeError`; currently unreachable since modules exist.) +- **Fix:** Delete the build-seam comment; either drop the guards (import directly) or have the tools raise `ToolError("engineering module unavailable: …")` when a symbol is None instead of calling it. + +### N10 — `CommandResult` dataclass + `to_dict` never instantiated +- **Severity:** SMELL +- **Location:** `backends/base.py:26-38`; exported `backends/__init__.py:2, 8` +- **Description:** Defined and re-exported, but no constructor call exists outside docs; every command return uses a plain dict. CLAUDE.md/README still list it as a shared dataclass. +- **Fix:** Delete it + its export + doc refs, or actually adopt it as the command return type. + +### N11 — `_ACI_NAMES` color-name table defined but never referenced +- **Severity:** SMELL +- **Location:** `backends/ezdxf_backend.py:54-57` +- **Fix:** Delete, or wire into `_entity_info_dxf`/`_layer_info_dxf` as a `color_name` field. + +### N12 — `AutoCADBackend.set_layer_active` alias has zero callers +- **Severity:** SMELL +- **Location:** `backends/base.py:910-912` +- **Fix:** Remove; callers use `layer_set_current` directly. + +### N13 — `AutoCADBackend.ensure_linetypes` concrete helper is dead (superseded by `engineering.layers.ensure_standard_linetypes`) +- **Severity:** SMELL +- **Location:** `backends/base.py:914-934` +- **Description:** Concrete ABC method with no callers; the real bootstrap path (`server.py:288`, `layers.py:110`) calls the module-level `ensure_standard_linetypes`, which reimplements the same already_loaded/loaded/failed loop. +- **Fix:** Delete it, or refactor `ensure_standard_linetypes` to delegate to it (single owner). + +### N14 — COM `self._batch_mode` instance attr written but never read (`_regen` consults only `_COM_STATE`) +- **Severity:** SMELL +- **Location:** `backends/com_backend.py:363, 889, 907, 916, 933` vs `_regen` `180-187` +- **Description:** `_regen` guards on `_COM_STATE.get("batch_mode")` only. The array methods set *both* `self._batch_mode` and `_COM_STATE['batch_mode']`; the instance attr has no readers and can desync. +- **Fix:** Remove the redundant `self._batch_mode` assignments (keep the dict flag), or make `_regen` read the instance attr and drop the dict — one source of truth. + +### R27 — Validator step 5 uses substring matching on all TEXT-layer text +- **Severity:** SMELL (downgraded from LOW — gated by `if helix:`, only runs when caller declares `helix_angle`; flagging a SPUR title on a declared-helical part is intended; only the over-broad substring scan is a smell) +- **Location:** `engineering/validator.py:159-181` (`:168`, `:175`) +- **Fix:** Restrict the scan to title-block text and use word-boundary regex (`\bSPUR\b`/`\bHELICAL\b`) instead of `in`. + +### R25-residual — `block_find_references` INSERT cap +- **Severity:** SMELL (memory-bug claim REFUTED — `entity_list` defaults to `limit=200`, hard-capped; see appendix) +- **Location:** `server.py:1368-1369` +- **Fix (optional):** Warn when INSERT count hits the 200 cap to avoid silently under-reporting references in large drawings. + +--- + +## (a) Count Summary by Severity + +| Severity | Count | IDs | +|---|---|---| +| CRITICAL | 0 | — | +| HIGH | 2 | N1, N2 | +| MED | 13 | R4, R5, R6, R9, R12, R13, R15, R18, R19, R22, N3, N4, N5, N6 *(14 listed — see note)* | +| LOW | 9 | R16, R20, R23, R24, R26, R28, R21, N7, N8, N9 *(10 listed — see note)* | +| SMELL | 13 | R17, R29, R25, R27, S1, S2, S3, S4, S5, N10, N11, N12, N13, N14 *(14 — see note)* | + +> Note: counts reflect merges (R10+R11 → N2; R23 split across N6+R23; R13 absorbs two fresh-sweep offset findings; R10 residual folded into N2). Exact kept-finding totals: **HIGH 2, MED 14, LOW 10, SMELL 14 = 40 kept**; 3 dropped (refuted/already-fixed appendix below). + +--- + +## (b) Confirmed P1 Shortlist — highest-leverage fixes to tackle next + +1. **N1 — COM hatch associativity/delete bug** (`com_backend.py:659-673`). Flip `AddHatch(…, True)` → `False`. One-line fix, prevents corrupt/invalid hatches in live AutoCAD. +2. **N2 — unify `drawing_save_as` format + derive from extension in `drawing_finalize`** (`base.py:158`, `ezdxf_backend.py:346`, `com_backend.py:516`, `server.py:2299`). Fixes silent DXF-as-DWG mislabeling and cross-backend format divergence on the primary output path. +3. **R19 — fix the two ezdxf dimension calls + make xfail strict** (`ezdxf_backend.py:696, 711-715`; `tests/test_dimensions.py:16,23`). Two advertised tools ship broken; cheap, high-visibility. +4. **R4 — make `dim_overlap` actually run** (`critique.py:199-201` + extractors). Restores a premium-workflow safety check that is currently a guaranteed no-op. +5. **R18 — surface bootstrap failure in `drawing_new`** (`server.py:282-293`). Prevents silent production-precondition violations (geometry on missing layers). +6. **N4 — parameterize `dimension_auto` / construction layers by active layer set** (`base.py:822/836/845/852, 743, 754`). Fixes ISO-13567 dims/construction landing on wrong auto-created layers. +7. **R9 — fix full-circle polar array overlap + COM divergence** (`ezdxf_backend.py:947`). Eliminates duplicate-entity output and a backend behavior split. +8. **R6 — add a mocked COM unit-test suite** (`tests/test_com_backend.py`). Unlocks regression safety for 1651 lines / 85 methods that currently can't be caught on CI — force-multiplier for every other COM fix above. +9. **R5 — bound the COM timeout thread/apartment leak** (`com_backend.py:416-434`). Prevents long-session resource exhaustion in live AutoCAD. +10. **R13/N5 — `entity_offset` side params + property-shape parity** (`ezdxf_backend.py:869-909`, `com_backend.py:861-875`; props extractors). Removes two dead documented params and the cross-backend `properties` contract drift that breaks downstream clients. + +--- + +## (c) Refuted / Already-Fixed Appendix (checked, not actioned) + +- **R1 — premium tools missing on COM:** ALREADY FIXED in `cd6329a`. The 8 premium meta-tools are now concrete methods on `AutoCADBackend` (`backends/base.py`); only `_create_xline` is backend-specific. +- **R2 — `int(lineweight)` truncation:** ALREADY FIXED in `cd6329a` via `normalize_lineweight()` in `base.py` (reconciles mm-float vs hundredths-int). +- **R3 — `iso128` critique no-op:** ALREADY FIXED in `cd6329a`. +- **R8 — _(no verification payload supplied; not assessed)._** Listed only for register completeness. +- **R14 — engineering import-guard `TypeError`:** PARTIAL / **not reachable**. Both `titleblock.py` and `validator.py` exist and import cleanly, so the `None`-fallback branch is never taken in the shipped repo. The vestigial guards/comment are carried as **S4** (SMELL). +- **R25 — `block_find_references` unbounded INSERTs + KeyError:** REFUTED. `entity_list` defaults to `limit=200` (hard-capped at all three layers), the opposite of "unbounded"; `e.properties.get("block_name")` cannot `KeyError`. Optional cap-warning carried as a SMELL above. +- **R29 — COM broad `except Exception` masks disconnects:** REFUTED as stated (reconnect not bypassed; most sites log). Retained as SMELL (narrow the guard types). +- **R17 — `_safe_send_command` deadline decoupling as a defect:** the defect framing is REFUTED (inner deadline is the tighter, primary one); retained as SMELL. +- **S4 (`Task #7/#8` phrasing):** the specific cited phrase is REFUTED (the comment says "Agent B … still in flight," not "Task #7/#8"); the substantive build-seam-comment smell STANDS and is kept as S4. diff --git a/docs/analysis/02-ROADMAP.md b/docs/analysis/02-ROADMAP.md new file mode 100644 index 0000000..280aa94 --- /dev/null +++ b/docs/analysis/02-ROADMAP.md @@ -0,0 +1,146 @@ + + +# AutoCAD MCP Pro — Evidence-Based Roadmap + +> Mission: become the best AutoCAD MCP for LLM-driven drawing. The durable moat is the **standards-validation gate** (no competing CAD MCP — build123d / FreeCAD / Blender / Rhino / Onshape — ships any design-rule validation). Everything below is sequenced to widen that moat or to remove the silent no-ops that make it lie. + +Priority signal = `(impact + differentiation + evidence_strength) / effort`, computed on **post-challenge adjusted** scores, then adjusted by judgment (quick wins and dependency order). DEFER → Backlog; RESCOPE → adjusted scope used; KEEP → as scored. + +--- + +## Scoring table (all ideas, adjusted, sorted by priority signal) + +| ID | Title | Imp | Eff | Diff | Ev | Signal | Verdict | Area | +|----|-------|-----|-----|------|----|--------|---------|------| +| **I9** | Expand deterministic geometry surface | 3 | 2 | 3 | 5 | **5.5** | KEEP→rescoped* | determinism | +| **I3** | ProCAD pre-plan clarification pass | 3 | 2 | 4 | 4 | **5.5** | RESCOPE | closed-loop | +| **I16** | Correctness + hygiene bundle (DIAGNOSTICS) | 3 | 2 | 2 | 5 | **5.0** | KEEP | hygiene | +| **I6** | GD&T primitive (ISO 1101 / Y14.5) | 5 | 3 | 5 | 3 | **4.3** | KEEP (impact↑) | iso-production | +| **I14** | NL→primitive grounding (trim/fillet/chamfer) | 3 | 3 | 3 | 4 | **3.3** | RESCOPE | determinism | +| **I15** | Staged validator (code→geometric→rubric) | 5 | 3 | 5 | 5 | **5.0** | RESCOPE | metrics | +| **I12** | Authenticated remote HTTP | 2 | 1 | 2 | 2 | **6.0** | KEEP (quick win) | infra | +| **I4** | Scalar drawing-score / Invalidity-Ratio | 3 | 2 | 3 | 4 | **5.0** | KEEP | metrics | +| **I2** | Visual critique / render as ImageContent | 3 | 2 | 4 | 5 | **6.0** | RESCOPE (cheap half) | closed-loop | +| **I18** | Real drawing repair (recover + Auditor.fix) | 2 | 2 | 1 | 3 | **3.0** | KEEP | robustness | +| **I1** | Routed iterative Refiner loop | 4 | 4 | 5 | 5 | **3.5** | RESCOPE | closed-loop | +| **I5** | ISO 129 tolerances on dimensions | 4 | 4 | 3 | 5 | **3.0** | RESCOPE | iso-production | +| **I8** | ISO-129 DimStyle + MLEADER | 3 | 3 | 2 | 3 | **2.7** | as scored | iso-production | +| **I19** | COM test harness (mocked) + cov gate | 2 | 2 | 1 | 3 | **3.0** | KEEP | hygiene | +| **I7** | Real plotted sheets + real DWG | 3 | 3 | 3 | 3 | **3.0** | RESCOPE (cheap half) | iso-production | +| **I17** | Fast selection via native query | 2 | 3 | 1 | 2 | **1.7** | ezdxf half only | perf | +| **I13** | Standard-parts / symbol library | 3 | 4 | 3 | 2 | **2.0** | RESCOPE | breadth | +| **I11** | Parametric editability / conv. edit | 3 | 4 | 3 | 2 | **2.0** | as scored | breadth | +| **I10** | 3D solids tier (or fix false claim) | 2 | 4 | 2 | 2 | **1.5** | split | breadth | + +\* I9/I3 share the top signal; I9 edges first because it is zero-risk and directly converts the GeoGramBench failure mode (<50% program-to-geometry) into deterministic tool calls. + +--- + +## Sprint 1 — Quick wins & correctness + +Foundation: the closed-loop moat only pays off if the validator/critique signals are not silent no-ops. Land the cheap, CI-verifiable correctness fixes and the highest-signal determinism wins first. + +### I16 — Correctness + hygiene bundle (DIAGNOSTICS fixes) · imp 3 / eff 2 / diff 2 +**Why now:** Every premium-workflow signal that the moat depends on is currently degraded by confirmed defects (fake DWG, broken aligned/angular dims, polar-array dup, silent bootstrap). Iteration evidence (Self-Improving CAD +13.4pp/round) only pays off if signals aren't dead. **Depends on:** land alongside I19 (COM-only N1/R5 need a harness). +**First step:** One PR for the CI-verifiable wins: fix R19 (`ezdxf_backend.py:696` `dist`→positional `distance`; `:711` `add_angular_dim_2l(base=…, line1=…, line2=…, location=(tx,ty))`), flip `tests/test_dimensions.py:16,23` to `xfail(strict=True)`, fix R9 divisor (`:947`), R12 `location=(tx,ty)` (`:715`), R13 offset side via cross-product sign; then `ruff check --fix .` (41 auto) + 6 manual. Verified: ezdxf `drawing_save_as` ignores `fmt` and returns `{"format": fmt}` while writing DXF bytes (`ezdxf_backend.py:346-353`) — make it derive fmt from extension and refuse to mislabel. + +### I9 — Expand deterministic geometry surface · imp 3 / eff 2 / diff 3 +**Why now:** Highest priority signal. `point_from_snap` is a **concrete** base method sourcing from the shared `EntityInfo.properties` contract (verified at `base.py:585`), so new snaps are pure math in one place, zero per-backend code, free on COM+ezdxf. Finishes the pre-committed `int` stub (`plan_spec.py:24`). **Depends on:** none. +**First step:** Implement the deferred `point_intersection` (line-line / line-circle / circle-circle, ref-point disambiguation) and `point_tangent` (external-point-to-circle — highest leverage). Demote/drop `point_offset` (trivial vector add). Either cover LWPOLYLINE or document the gap explicitly. Dual-backend tests in `tests/test_meta_tools.py`. + +### I12 — Authenticated remote HTTP · imp 2 / eff 1 / diff 2 — **QUICK WIN / latent security bug** +**Why now:** Confirmed: no `auth=` arg and no `TokenVerifier` anywhere (grep returned nothing). `_validate_http_bind` promises bearer-token auth but it is **never enforced** — a remote operator who sets `ALLOW_REMOTE_HTTP`+`MCP_AUTH_TOKEN` gets an unauthenticated server that believes it is authenticated. FastMCP 3.0.1 ships `StaticTokenVerifier`. ~10 lines. +**First step:** Build `_auth = StaticTokenVerifier(tokens={settings.mcp_auth_token: {...}}) if settings.mcp_auth_token else None`, pass `auth=_auth` into `FastMCP(...)` at `server.py:153`; add `tests/test_http_auth.py` (401 on bad/missing token, pass on correct). + +### I12-adjacent quick win — I2 cheap half (render as ImageContent) · imp 3 / eff ~1 / diff 4 +**Why now:** `fastmcp Image` is already imported/returned by `view_screenshot` (`server.py:1846+`); `drawing_finalize` writes screenshot bytes to disk and discards them. Returning the existing PNG bytes as `ImageContent` lets the already-multimodal driving agent (Claude) see the render — captures the CADSmith "render is a first-class signal" win with **no model bundling, no torch, no [vision] extra**. +**First step:** Add `return_image`/`include_render` to `drawing_finalize` (or a thin `visual_inspect` tool) that wraps existing bytes in `Image`. Keep the deterministic critique gate pure — the visual signal is advisory, **not** a `CritiqueFocus` member. Defer the local-VLM judge and OCR dim round-trip. + +--- + +## Phase A — Own the moat (closed-loop validation) + +This is the differentiation core. Sequence: scalar metric → staged unified validator → clarification gate → refiner loop. The metric makes the loop measurable; the unified validator fixes a real disconnect; the refiner acts on it. + +### I4 — Scalar drawing-score / Invalidity-Ratio · imp 3 / eff 2 / diff 3 +**Why now:** No quantitative metric exists today; `drawing_critique` returns a list, `validator.run` returns severity counts only. MUSE/CadBench grade Invalidity-Ratio, not shape. A scalar is the objective function that makes the repair loop measurable and the smoke test regression-trackable. **Depends on:** sequence after I16 (so the score isn't averaged over dead checks like R4 dim_overlap). +**First step:** Add `engineering/scoring.py::score_findings(severity_counts)` → `score = 100 − (errors·Wₑ + warnings·Wᵥᵥ + info·Wᵢ)` clamped, plus `invalidity_ratio = errors/max(1, findings)`. Consume from both `validator.py` and `critique.py`; surface in `drawing_finalize` payload. Document weights as a convention, not ground truth. + +### I15 — Staged validator (code→geometric→rubric) · imp 5 / eff 3 / diff 5 — RESCOPE +**Why now:** Strongest hidden find: `drawing_finalize` (`server.py:2311`) calls **only** `DrawingValidator.run()` and **never** `run_critique` — the 6 premium critique focuses are not part of the finalize gate. Unifying validator's 8 steps + critique's 6 focuses into one staged protocol fixes a real architectural disconnect, not just cosmetics. **Depends on:** the no-op critique fixes (R4/R12/R19 in I16) must land first or the staged scalar lies. Drop R27 (separate SMELL); defer the VLM rubric stage to I2's deferred half. +**First step:** Define `StagedReport` (stages: code/geometric) with `{rule_id, stage, severity, passed, handles, detail}` + the I4 scalar; refactor `DrawingValidator.run` to tag findings with stage and reuse `_FOCUS_DISPATCH` for the geometric stage; wire **both** into `drawing_finalize`. Assert the scalar in `scripts/smoke_premium.py`. + +### I3 — ProCAD pre-plan clarification pass · imp 3 / eff 2 / diff 4 — RESCOPE +**Why now:** ProCAD's one clarification round cut invalidity 14.6%→0.9% — attacks "garbage in" before any geometry, which no downstream critique recovers. `drawing_plan` is a thin concrete method building a `PlanSpec` (`base.py:553`). **Depends on:** none, but deep material/tolerance NL-extraction should wait for the I5 tolerance model. **Rescope:** ship cheap robust checks only — PlanSpec has no structured tolerance/material fields, so deep NL-parsing balloons effort. +**First step:** `engineering/plan_spec.py::detect_ambiguities(plan)` mirroring `_FOCUS_DISPATCH`: rule 1 = `view_count` vs intent keywords (section/orthographic/3-view); rule 2 = scale-vs-sheet sanity; rule 3 = ordinate dim_style needs a datum note. Surface advisory `clarifications` in the server wrapper return. + +### I1 — Routed iterative Refiner loop · imp 4 / eff 4 / diff 5 — RESCOPE +**Why now:** The validation-gate-as-repair-loop is the moat no competitor has, directly operationalizing CADSmith Refiner (38x) and FEA-agent (+13.4pp/round). All repair primitives exist on both backends with transaction rollback. **Depends on:** I15 (unified staged report) and I16 (R4/R19). **Rescope to honest v1:** auto-repair only the 3 clean deterministic focuses + iso128-snap; route `untrimmed_corner` and `dim_overlap` to the LLM as **hints**, not auto-fixes. +**First step:** `engineering/refine.py` with a `REPAIR_HINTS` registry mapping each `CritiqueFocus` to (a) a deterministic repair coroutine and (b) a fallback hint. Implement the 4 trivial focuses (`construction_left`→`construction_clear`, `duplicate_entities`→`entity_delete(handles[1])`, `layer_color`→`layer_modify(color=detail.expected)`, `iso128`→snap-to-nearest-ISO). Then `drawing_refine(max_rounds=4)` looping critique→repair→re-critique. **Critical blocker (R22):** the ezdxf `_undo_stack` is shared between user undo and transactions with no txn-id isolation (`system_status` even reports `len(_undo_stack)` as transaction_depth) — add a dedicated txn list/guard before the rollback cycle, or it corrupts undo state. + +--- + +## Phase B — Production ISO parity + +Make output actually manufacturable. ISO 129 tolerances are the single biggest functional gap; GD&T is the highest-differentiation breadth item. + +### I6 — GD&T primitive (ISO 1101 / Y14.5) · imp 5 / eff 3 / diff 5 — KEEP (impact↑) +**Why now:** Maximal differentiation — 2D GD&T is absent from every surveyed text-to-CAD product and every competing MCP. Pure composition over existing primitives (the `titleblock_apply_iso_a3` pattern, `server.py:2248`) — no ABC/dual-backend additions. Impact raised to 5: bundle a thin tolerance-text option and one GD&T critique check (datum letter in an FCF must have a matching datum feature) to convert it from "draws a symbol" to "validates a standard." **Depends on:** none (composes cleanly). +**First step:** `engineering/gdt.py` with the 14 ISO 1101 characteristic glyphs + Ⓜ/Ⓛ/Ⓢ modifiers (unicode/MTEXT codes), then `draw_feature_control_frame(...)` composing the frame from `entity_create_line` + `entity_create_text` on DIM layer. Register one `gd_frame` `@mcp.tool`. **Design constraint:** avoid ezdxf's native Tolerance/FCF entity (renders blank through matplotlib) — compose from lines+text. Smoke-test glyph rendering via screenshot. + +### I5 — ISO 129 tolerances on dimensions · imp 4 / eff 4 / diff 3 — RESCOPE +**Why now:** A drawing without ±/limit/fit callouts is not manufacturable — the dossier's "biggest functional gap." Empirically verified ezdxf 1.4.3 renders sym/limit/text overrides via override dict. **Depends on:** R19 fix (I16) is a soft prerequisite — aligned/angular dims are currently broken, so don't bolt tolerances onto them yet. **Rescope:** ezdxf-first, CI-testable subset; defer aligned/angular tolerances behind R19; COM tolerance rendering best-effort/untested until I19. +**First step:** Add `tol_upper/tol_lower/tol_mode/fit/text_override` to `dimension_linear/radius/diameter` in `base.py`; implement in ezdxf via override dict (sym/limit → DIMTOL/DIMLIM + DIMTP/DIMTM; fit → `engineering/iso286_fits.py` nominal-band×grade table — the main authored-data cost). Test asserts the DIMENSION carries DIMTP/DIMTM and reads e.g. `20 +0.021/0`. + +### I8 — ISO-129 DimStyle + MLEADER · imp 3 / eff 3 / diff 2 — as scored +**Why now:** Every dim today inherits the default "Standard" style (no DIMTXT/DIMASZ/precision). Natural sibling to `drawing_apply_iso_layers`. Low differentiation (table-stakes) — sequence after I5/I6. **Depends on:** I5 (share the dimstyle plumbing). +**First step:** `engineering/layers.py::apply_iso_dimstyle(backend)` → `doc.dimstyles.new("ISO-25", …)`; thread `dimstyle="ISO-25"` through ezdxf dim methods; wire into `drawing_apply_iso_layers`. Defer COM `DimStyles.Add` and the MLEADER tool to follow-ups. + +### I7 — Cheap half: fmt-from-extension + true ISO-128 lineweights · imp 3 / eff 3 / diff 3 — RESCOPE +**Why now:** Two isolated wins. (1) Fake-DWG is ezdxf-only (COM already writes real DWG via `SaveAs(path,12)`); (2) ISO-128 true lineweights via ezdxf `Configuration` (`lineweight_policy`) on the render path is the genuinely differentiating piece and feeds the render/critique signal. **Defer** the from-scratch paperspace/viewport/Plot pipeline (multi-day, CI-unverifiable, `odafc` needs the ODA binary — confirmed not on PATH) to the backlog. **Depends on:** none. +**First step:** Wire `Configuration(lineweight_policy=…)` into the PDF/screenshot Frontend; unify the save_as fmt default and fail loud when ezdxf can't write real DWG (use `odafc.export_dwg` if present, else raise — never mislabel). + +--- + +## Phase C — Breadth & determinism extras + +Catch-up capability and convenience. Lower priority signals; do not displace the moat work. + +### I14 — NL→primitive grounding for trim/fillet/chamfer · imp 3 / eff 3 / diff 3 — RESCOPE +**Why now:** Repair-side companion to the `untrimmed_corner` detector; attacks FutureCAD primitive-grounding + GeoGramBench. **Rescope:** "which corner" is net-new work — `analysis_bounding_box` returns only a global bbox, so corner→handle mapping re-runs O(n²) endpoint clustering. Ship ezdxf path with bbox-quadrant corner naming that **errors with candidates when ambiguous** instead of guessing; defer COM until I19 lands COM tests. **Depends on:** I9 (intersection math), I19 (COM verification). + +### I18 — Real drawing repair (recover + Auditor.fix) · imp 2 / eff 2 / diff 1 — KEEP +**Why now:** Confirmed honesty bug: `drawing_audit` returns only `auditor.errors`, ignores `auditor.fixes`, while the docstring claims it "fixes errors"; `drawing_open` uses bare `readfile` with no `recover.readfile` fallback. Near-free robustness win. +**First step:** Return `errors`+`fixes`+`fixed_count` and `_mark_dirty()` when fixes applied; wrap `readfile` with a `recover.readfile` fallback. + +### I19 — COM test harness (mocked) + scoped coverage gate · imp 2 / eff 2 / diff 1 — KEEP +**Why now:** R6 confirmed — zero tests touch `ComBackend`; CI is ubuntu-only so 1651 lines never run. `normalize_lineweight` (the P0 anti-truncation fix, pure/OS-agnostic) has no regression guard. Unblocks COM verification for I14/I5/I16. **Depends on:** none; should land **before/with** I16's COM-only items (N1/R5). +**First step:** `tests/test_com_backend.py` with a `sys.modules` fake-COM shim; parametrized `normalize_lineweight` table tests + `_safe_send_command` CMDACTIVE/timeout/var-restore tests. Scope `--cov-fail-under` to `backends.base` + tested modules, not repo-wide. + +### Backlog / Later (!) + +- **I17** — Fast selection: do the **ezdxf-only** half (`query()`/`groupby()` one-liners, zero-risk, headless-testable); defer the COM `SelectAll` rewrite (CI-unverifiable, finicky compound filters). Not a moat-widener. +- **I13** — Standard-parts library: ship the cheap `library_insert` wrapper + a few parametric ISO bolt block defs; the real cost (ISO-correct fastener + P&ID symbol authoring) is large ongoing work. Defer dynamic-block driving (COM-only, breaks dual-backend parity). +- **I11** — Parametric editability: ship single-entity `entity_edit_geometry` (set radius/endpoints) first; true re-drive needs a constraint graph the architecture has no scaffold for. Catch-up, not moat. +- **I10** — 3D solids: **do the near-zero-effort honesty fix now** (correct the false `"all_entity_types"` capability at `com_backend.py:1444`). Defer the COM-only solids tier; ezdxf has no ACIS kernel so it breaks dual-backend parity, and all cited research targets 2D fidelity. +- **I7 expensive half** — paperspace/viewport/Plot pipeline + bundled-VLM judge. + +--- + +## Recommended next 5 work items + +1. **I16 (Correctness bundle) + I19 (COM harness) together.** Highest evidence strength; everything in Phase A is built on signals that are currently silent no-ops (R4 dim_overlap, R19 broken dims, fake DWG). The COM harness lands with it so the COM-only fixes (N1, R5) and `normalize_lineweight` are actually guarded. Foundation for the entire moat. +2. **I9 (Deterministic geometry surface).** Top priority signal, zero risk, one-place pure math. Directly converts the GeoGramBench <50% program-to-geometry failure into deterministic tool calls — the cheapest moat-adjacent win available. +3. **I12 (HTTP auth) + I2-cheap (render as ImageContent).** Two quick wins. I12 closes a real false-security gap (~10 lines, verified no auth wiring exists). I2-cheap captures the CADSmith "render is a first-class signal" win for ~1 day with no model bundling, by returning the already-produced PNG to the multimodal driver. +4. **I4 → I15 (Scalar metric → unified staged validator).** I4 gives the objective function; I15 fixes the confirmed disconnect where `drawing_finalize` never runs the premium critique focuses. Together they make the moat measurable, regression-testable, and actually enforced at finalize — the prerequisite for the refiner. +5. **I6 (GD&T primitive).** Highest-differentiation feature in the dossier, KEEP at effort 3, impact raised to 5 by bundling a tolerance-text option and one datum-consistency critique. Pure composition, no backend surface change — the single biggest "no competitor has this" capability. + +Sequencing rationale: items 1–4 are explicitly ordered by `depends_on` (signals must be honest before the scalar averages them; the scalar must exist before the refiner can optimize it; R22 txn-isolation gates the refiner's rollback cycle). Item 5 is parallelizable — it composes cleanly and shares no dependency with the closed-loop track, so it can run alongside Phase A. + +--- + +## Security note — unverified DIAGNOSTICS items (R7/R8) + +The diagnostics flagged two security items that the security-review subagents **could not verify**: **R7** (input-sanitizer wiring — whether `system_run_command` / `system_run_lisp` user strings are actually routed through a sanitizer before reaching `SendCommand`) and **R8** (absence of adversarial sanitizer tests). These sit on the live-COM command path, which is the highest-blast-radius surface in the repo (arbitrary AutoLISP/command execution) and is exactly the path with **zero COM test coverage** (R6). + +**Recommendation — add a defensive hardening work item (slot in Sprint 1, alongside I19):** (a) trace `system_run_command`/`system_run_lisp` end-to-end and confirm every user-supplied string passes through an explicit allow/deny sanitizer before `_safe_send_command`; if no sanitizer is wired, add one (reject embedded newlines, `\x03`/ESC injection, nested `(command …)` escapes, and `SCRIPT`/`SHELL`/`START` command words). (b) Add adversarial unit tests on the mocked-COM harness from I19 asserting that known injection payloads are rejected before reaching `SendCommand`. This is cheap insurance on the one surface where a prompt-injected agent could execute arbitrary OS-level actions via `SHELL`/`START`, and it composes directly with the I19 fake-COM fixture. diff --git a/docs/analysis/_recon_raw.json b/docs/analysis/_recon_raw.json new file mode 100644 index 0000000..f71c20f --- /dev/null +++ b/docs/analysis/_recon_raw.json @@ -0,0 +1,1521 @@ +{ + "moduleMaps": [ + { + "module": "server.py (AutoCAD MCP Pro FastMCP 3.0 server, 2881 lines)", + "purpose": "Single-file FastMCP server that exposes the full AutoCAD automation surface: 108 @mcp.tool registrations, 6 @mcp.resource endpoints, and 5 @mcp.prompt templates, all dispatching to a dual-backend (COM live AutoCAD / ezdxf headless) singleton selected at startup. Tools are grouped into 13 numbered sections (1-12 plus a Premium meta-tools section 13, with sub-sections 8b/8c/8d). Each tool is a thin async wrapper that resolves the backend via _backend(ctx), calls a same-named backend method, and serializes dataclass returns via _dc().", + "public_surface": [ + "mcp = FastMCP(name='AutoCAD MCP Pro', instructions=..., lifespan=autocad_lifespan) [server.py:153]; middleware order added: ErrorHandlingMiddleware -> AuditMiddleware -> TimingMiddleware -> LoggingMiddleware [server.py:197-200]", + "autocad_lifespan(server) [server.py:107] — @lifespan-decorated; calls _make_backend(), yields {'backend': backend} or {'backend': None, 'init_error': str} on failure; logs unsafe-mode warning if dangerous_commands_enabled", + "_make_backend() [server.py:69] — reads AUTOCAD_MCP_BACKEND env (auto|com|ezdxf); Windows tries ComBackend then falls back to EzdxfBackend; non-Windows always ezdxf", + "_backend(ctx) [server.py:207] — pulls ctx.lifespan_context['backend']; raises ToolError('AutoCAD backend unavailable: ') if None", + "_dc(obj) [server.py:216] — recursive dataclass->dict via asdict; lists handled; passthrough otherwise", + "_registered_tool_count() [server.py:225] — counts mcp._local_provider._components keys starting 'tool:'; returns -1 on exception; used by system_status/system_about", + "AuditMiddleware.on_call_tool [server.py:135] — times every tool, logs TOOL OK / TOOL ERR", + "SECTION 1 Drawing Mgmt (11): drawing_info->drawing_info; drawing_new->drawing_new(+engineering.ensure_standard_linetypes/ensure_engineering_layers when bootstrap); drawing_open->drawing_open; drawing_save->drawing_save; drawing_save_as->drawing_save_as; drawing_export_dxf->drawing_export_dxf; drawing_export_pdf->drawing_export_pdf; drawing_purge->drawing_purge; drawing_audit->drawing_audit; drawing_close->drawing_close; drawing_undo->drawing_undo; drawing_redo->drawing_redo (NOTE: 12 tools present, not 11)", + "SECTION 2 Entity Creation (13): entity_create_line/circle/arc/polyline; entity_create_rectangle->entity_create_polyline (wrapper); entity_create_text/mtext/hatch/spline/ellipse/point/block_ref -> same-named backend method", + "SECTION 3 Dimensions (5): dimension_linear/aligned/angular/radius/diameter -> same-named", + "SECTION 4 Entity Modification (10+4 corner): entity_move/copy/rotate/scale/mirror/offset; entity_trim/extend/fillet/chamfer (corner ops); entity_delete; entity_array_rectangular/polar; entity_set_properties -> same-named", + "SECTION 5 Entity Query (3): entity_get->entity_get; entity_list->entity_list; entity_delete_many [server.py:1065] -> loops backend.entity_delete per handle (server-side, has try/except per item)", + "SECTION 6 Layer (12+2): layer_list/create/delete/set_current/modify/freeze/thaw/lock/unlock/hide/show -> same-named; layer_isolate [server.py:1216] -> server-side loop over layer_list+layer_hide; linetype_list/linetype_load -> same-named", + "SECTION 7 Block (7): block_list/insert/explode/get_attributes/set_attributes/create_from_entities -> same-named; block_find_references [server.py:1362] -> backend.entity_list(type_filter='INSERT') filtered by properties['block_name'] server-side", + "SECTION 8 Analysis (8): analysis_entity_stats->analysis_stats; analysis_find_in_region->analysis_entities_in_region; analysis_measure_distance->analysis_measure_distance(+server angle calc); analysis_measure_area->analysis_measure_area(+server perimeter); analysis_bounding_box->analysis_bounding_box; analysis_select_by_layer->analysis_select_by_layer; analysis_select_by_type->analysis_select_by_type; analysis_layer_stats [server.py:1506] -> composes layer_list+entity_list server-side", + "SECTION 8b Batch (2): entity_batch_create [server.py:1538] -> dispatch dict to backend.entity_create_* ; entity_batch_modify [server.py:1590] -> dispatch to entity_move/rotate/scale/delete/set_properties", + "SECTION 8c Templates (2): template_apply_layers [server.py:1695] -> _LAYER_TEMPLATES + backend.layer_create loop; template_list [server.py:1730] -> pure in-memory", + "SECTION 8d Validation (1): validation_check [server.py:1749] -> composes layer_list+entity_list server-side", + "SECTION 9 View (4): view_zoom_extents->view_zoom_extents; view_zoom_window->view_zoom_window; view_screenshot->view_screenshot; view_zoom_and_screenshot->view_zoom_window/extents + view_screenshot (NOTE: 4 tools, CLAUDE.md says 5)", + "SECTION 10 Transactions (3): transaction_begin/commit/rollback -> same-named", + "SECTION 11 System (6): system_status->system_status(+tool_count/unsafe_mode); system_get_variable->system_get_variable; system_set_variable->system_set_variable; system_run_command->sanitize_command+system_run_command; system_run_lisp->sanitize_lisp+system_run_lisp; system_about [server.py:2061] -> static tool_groups dict + _registered_tool_count", + "SECTION 12 Engineering (7): gear_draw_helical_front_view->engineering.draw_helical_gear_front_view; gear_draw_spur_front_view->draw_spur_gear_front_view; gear_draw_section_aa->draw_gear_section_aa; keyway_draw_keyed_bore->draw_keyed_bore; keyway_draw_section->draw_keyway_section; titleblock_apply_iso_a3->apply_iso_a3_titleblock; drawing_finalize->DrawingValidator().run + backend.drawing_save_as/view_screenshot", + "SECTION 13 Premium meta (8): drawing_plan->drawing_plan; drawing_critique->drawing_critique; point_from_snap->point_from_snap; construction_xline->construction_xline; construction_clear->construction_clear; drawing_apply_iso_layers->drawing_apply_iso_layers; dimension_auto->dimension_auto; entity_select_smart->entity_select_smart", + "RESOURCES (6): autocad://drawing/info -> drawing_info; autocad://layers -> layer_list; autocad://blocks -> block_list; autocad://entities/stats -> analysis_stats; autocad://system/status -> system_status; autocad://entities/{layer_name} -> analysis_select_by_layer", + "PROMPTS (5): prompt_floor_plan, prompt_pid_diagram, prompt_electrical_schematic, prompt_mechanical_drawing, prompt_quick_drawing (all pure string generators)", + "Entry point [server.py:2861]: manual argv parse for --transport/--port/--host; _validate_http_bind() refuses non-loopback bind unless ALLOW_REMOTE_HTTP + MCP_AUTH_TOKEN set" + ], + "dependencies": [ + "fastmcp (FastMCP, Context, ToolError, lifespan, Middleware, ErrorHandlingMiddleware, LoggingMiddleware, TimingMiddleware, utilities.types.Image)", + "pydantic (Field)", + "config (config.settings: dangerous_commands_enabled, max_list_limit, allow_remote_http, mcp_auth_token)", + "security (sanitize_command, sanitize_lisp, validate_path)", + "backends.base.AutoCADBackend (ABC) + EntityInfo/LayerInfo/BlockInfo/DrawingInfo/CommandResult dataclasses", + "backends.ezdxf_backend.EzdxfBackend, backends.com_backend.ComBackend", + "engineering package (draw_*_gear_*, draw_keyed_bore, draw_keyway_section, apply_iso_a3_titleblock, TitleBlockMetadata, DrawingValidator, ensure_engineering_layers, ensure_standard_linetypes) — titleblock/validator imports are guarded and fall back to None", + "stdlib: json, logging, math, os, sys, time, dataclasses.asdict, win32gui (optional, AutoCAD window detection)" + ], + "key_flows": [ + "Startup: __main__ -> mcp.run() (stdio) or _validate_http_bind + mcp.run(transport,host,port) -> autocad_lifespan -> _make_backend() picks COM/ezdxf -> stored in lifespan_context['backend']", + "Per tool call: ErrorHandling -> Audit(timing) -> Timing -> Logging middleware -> tool fn -> _backend(ctx) -> backend method -> _dc() serialization", + "Premium production workflow (per CLAUDE.md): drawing_plan -> drawing_apply_iso_layers -> construction_xline -> entity_create_* -> entity_trim/fillet -> entity_select_smart -> dimension_auto -> drawing_critique (must be []) -> construction_clear -> drawing_finalize", + "Engineering workflow: drawing_new(bootstrap) -> titleblock_apply_iso_a3 -> gear_draw_*_front_view -> gear_draw_section_aa -> dimension_* -> drawing_finalize (8-step validator, raises ToolError on any error-severity finding)", + "Backend dispatch parity: server tools call identically-named backend methods; both EzdxfBackend and ComBackend declare all 8 premium meta-tool methods, but ComBackend's are NotImplementedError stubs" + ], + "risks": [ + { + "title": "All 8 Premium meta-tools are dead stubs on the COM backend (raise NotImplementedError)", + "severity": "critical", + "location": "backends/com_backend.py:1637-1660", + "detail": "ComBackend.drawing_plan/drawing_critique/point_from_snap/construction_xline/construction_clear/drawing_apply_iso_layers/dimension_auto/entity_select_smart all just `raise NotImplementedError(\"... pending Task #7/#8/#9\")`. On Windows with AutoCAD running (the default COM backend), the entire Premium workflow that CLAUDE.md declares NON-NEGOTIABLE for production output is non-functional: drawing_plan fails immediately, so drawing_critique (replays the plan) and drawing_finalize's quality gate cannot work. The server instructions and CLAUDE.md present these as the mandatory path; on the primary/default backend they 500. This is a dispatch/parity bug, not just incompleteness — the tools are registered and advertised as working." + }, + { + "title": "drawing_finalize calls drawing_save_as with only one positional arg, relying on differing default formats per backend", + "severity": "medium", + "location": "server.py:2299", + "detail": "`await backend.drawing_save_as(str(validated))` omits the fmt argument. EzdxfBackend default is fmt='dxf' (ezdxf_backend.py:345) but ComBackend default is fmt='dwg' (com_backend.py:515). So drawing_finalize(save_path='x.dwg') on ezdxf will try to write DXF logic to a .dwg path, and the format is silently backend-dependent rather than derived from the path extension. The save_as MCP tool itself defaults to 'dwg' (server.py:336), creating a third inconsistency. Likely produces wrong-format or mislabeled output files." + }, + { + "title": "drawing_new merges backend dict but engineering bootstrap can leave result without standard keys; non-dict raw silently wrapped", + "severity": "low", + "location": "server.py:280-294", + "detail": "`raw = await backend.drawing_new(template)`; if raw is not a dict it is wrapped as {'result': raw}. If a backend ever returns a non-dict (e.g. a dataclass), callers expecting drawing metadata keys get only {'result': ...} plus 'bootstrap'. Bootstrap failures are swallowed into result['bootstrap']={'error':...} and reported as success (no ToolError), so a fully un-bootstrapped drawing looks like a normal success to the client even though CLAUDE.md treats the engineering layer scaffold as a precondition for gear_*/titleblock_* tools." + }, + { + "title": "system_about hardcodes a tool_groups catalog that has drifted from actual registrations", + "severity": "medium", + "location": "server.py:2070-2106", + "detail": "The static tool_groups dict omits registered tools: entity_query lists only ['entity_get','entity_list'] but entity_delete_many is also registered (it is misfiled under entity_creation at line 2079). 'view' lists 4 tools but the count and grouping don't reflect that the dict is maintained by hand. There is no engineering (gear_*/keyway_*/titleblock_*/drawing_finalize) group and no premium (drawing_plan/critique/point_from_snap/construction_*/dimension_auto/entity_select_smart) group at all — 15 registered tools are entirely absent from system_about's self-description, so an agent inspecting capabilities via system_about will not see the very tools CLAUDE.md mandates." + }, + { + "title": "Engineering titleblock/validator symbols may be None at call time (guarded import), causing opaque failures", + "severity": "medium", + "location": "engineering/__init__.py:40-51", + "detail": "TitleBlockMetadata/apply_iso_a3_titleblock and DrawingValidator are imported in try/except ImportError that sets them to None. titleblock_apply_iso_a3 (server.py:2266) does `from engineering import apply_iso_a3_titleblock, TitleBlockMetadata` then calls them; if the underlying module failed to import, these are None and the call raises 'NoneType is not callable' rather than a clear ToolError. Same for DrawingValidator in drawing_finalize (server.py:2294,2311). The tool registers fine and only fails at invocation with an unhelpful message." + }, + { + "title": "_registered_tool_count reaches into FastMCP private internals", + "severity": "low", + "location": "server.py:231-234", + "detail": "Uses mcp._local_provider._components and string-prefix 'tool:' matching. Both are undocumented private attributes; a FastMCP minor upgrade can silently change them, making system_status/system_about report tool_count=-1 (the except fallback). Since CLAUDE.md says the count must come from here dynamically, a -1 would surface to clients as the tool count." + }, + { + "title": "view_zoom_extents/view_zoom_window registered for all backends but documented COM-only / no-op in ezdxf", + "severity": "low", + "location": "server.py:1814-1831", + "detail": "CLAUDE.md states view ops are no-ops in ezdxf. The tools are unconditionally registered and call backend.view_zoom_extents/window; on ezdxf these presumably return a no-op success, so an agent gets a success response implying the view changed when it did not. view_zoom_and_screenshot (server.py:1870) relies on these before screenshotting; on ezdxf the zoom_window path may not actually frame the requested region. Verify ezdxf view ops actually affect the matplotlib render extents." + } + ], + "smells": [ + { + "title": "Hardcoded/contradictory tool count: CLAUDE.md says 87 tools, file actually has 108 @mcp.tool registrations", + "severity": "high", + "location": "server.py (108 @mcp.tool) vs CLAUDE.md 'exposes 87 tools'", + "detail": "grep ^@mcp.tool = 108 registrations. CLAUDE.md project overview says '87 tools, 6 resources, and 5 prompt templates' and instructs never to hardcode the count. The code correctly computes it dynamically (_registered_tool_count) but the docs/section headers are stale: Section 1 header says '(11 tools)' but 12 drawing_* tools exist (drawing_redo at 426 is the 12th); Section 9 header says '(5 tools)' but only 4 view tools are registered. Doc-vs-code drift across multiple section banners." + }, + { + "title": "Six near-identical @mcp.resource bodies — copy-paste backend-None guard + try/except json.dumps", + "severity": "low", + "location": "server.py:2498-2607", + "detail": "resource_drawing_info, resource_layers, resource_blocks, resource_entity_stats, resource_status, resource_entities_by_layer all repeat the same pattern: fetch backend from lifespan_context, `if b is None: return json.dumps({'error':'Backend not ready'})`, try/except -> json.dumps(..., indent=2). Could be a single decorator/helper. Note they intentionally do NOT use _backend(ctx) (which would raise ToolError) — divergent error-handling convention between tools and resources." + }, + { + "title": "Five prompt templates duplicate the layer-set definitions that already live in _LAYER_TEMPLATES", + "severity": "low", + "location": "server.py:2614-2823 vs server.py:1647-1688", + "detail": "prompt_floor_plan / prompt_mechanical_drawing / prompt_pid_diagram / prompt_electrical_schematic embed hardcoded layer lists (names, colors, lineweights) that overlap with the _LAYER_TEMPLATES dict (architectural/mechanical/electrical/piping). Two sources of truth for the 'standard' layer sets; they can drift (e.g. prompt mechanical uses VISIBLE/HIDDEN/CENTER with linetypes, _LAYER_TEMPLATES mechanical uses all Continuous linetypes)." + }, + { + "title": "Section comment '(Task #7/#8/#9 implement these)' is a leftover TODO marker shipped in code", + "severity": "medium", + "location": "backends/com_backend.py:1636", + "detail": "Comment '# ── premium meta-tools (Task #7/#8/#9 implement these) ──' plus 8 NotImplementedError messages referencing 'pending Task #7/#8/#9'. These are in-flight development markers left in the committed COM backend; combined with the abstract base + ezdxf having real implementations, this is clear unfinished-work drift that the tool registrations and instructions hide." + }, + { + "title": "Two unrelated comments reference other 'Agents'/Tasks indicating multi-agent build seams left in", + "severity": "low", + "location": "engineering/__init__.py:37-39", + "detail": "Comment 'Agent B contributes titleblock + validator. Imports are guarded so this package remains usable while B is still in flight; once those modules land the symbols flow through transparently.' This, together with the com_backend Task #7/#8/#9 markers, shows the codebase was assembled by parallel agents and the seams (guarded imports to None, NotImplementedError stubs) were never reconciled before being presented as a finished 'production-grade' surface." + }, + { + "title": "block_find_references and analysis tools silently truncate / filter server-side without backend support", + "severity": "low", + "location": "server.py:1362-1370", + "detail": "block_find_references calls backend.entity_list(type_filter='INSERT') with no limit, then filters in Python on e.properties.get('block_name'). entity_list's own MCP wrapper caps at max_list_limit, but this server-side call bypasses that cap and could pull unbounded INSERTs into memory; also depends on every backend populating properties['block_name'] identically (parity assumption not enforced by the ABC)." + }, + { + "title": "Manual argv parsing in __main__ instead of using fastmcp's CLI / argparse", + "severity": "low", + "location": "server.py:2866-2880", + "detail": "--transport/--port/--host are parsed by hand via args.index(); no validation that the flag has a following value (IndexError if '--port' is last arg), no --help, and duplicates what `fastmcp run server.py:mcp` already provides (the module docstring even recommends the fastmcp CLI). Fragile and redundant." + } + ], + "notes": "Counts verified by grep: 108 @mcp.tool, 6 @mcp.resource, 5 @mcp.prompt. Backend parity confirmed: both backends implement every abstract method including the 8 premium meta-tools and 4 corner ops — but ComBackend's 8 premium methods are NotImplementedError stubs (com_backend.py:1637-1660), while EzdxfBackend has full implementations (ezdxf_backend.py:1753-2008). No tool dispatches to a method missing from a backend signature; the gap is stub-vs-real, which is worse because it passes registration and ABC checks. Engineering tools (Section 12) and Premium tools (Section 13) push the real total well above the documented 87. The 12-section taxonomy in CLAUDE.md is incomplete: actual sections are 1-12 plus 8b/8c/8d (batch/templates/validation) and 13 (premium) = 6 extra tool groups not in the canonical list." + }, + { + "module": "backends/base.py — AutoCADBackend ABC (THE CONTRACT)", + "purpose": "Defines the abstract interface every AutoCAD backend must implement plus the five shared serialization dataclasses (EntityInfo, LayerInfo, BlockInfo, DrawingInfo, CommandResult). It is the single source of truth for the strategy-pattern dual-backend architecture (EzdxfBackend, ComBackend). Two methods are concrete (set_layer_active, ensure_linetypes); the rest (~73) are @abstractmethod. Also exports three free helpers: shoelace_area, deg2rad, rad2deg.", + "public_surface": [ + "dataclass CommandResult(ok: bool, payload: Any = None, error: str|None = None) + to_dict() (base.py:26-38)", + "dataclass EntityInfo(handle, type, layer, color, linetype, visible, properties: dict = {}) (base.py:41-49)", + "dataclass LayerInfo(name, color, linetype, lineweight, is_on, is_frozen, is_locked, is_current) (base.py:52-61)", + "dataclass BlockInfo(name, origin: tuple[float,float], attribute_count, entity_count, is_xref, description: str = '') (base.py:64-71)", + "dataclass DrawingInfo(name, full_path, saved, entity_count, layer_count, block_count, extents_min, extents_max, units, version='', backend='') (base.py:74-86)", + "@property name -> str (base.py:98)", + "@property is_connected -> bool (base.py:102)", + "async connect() -> None (base.py:106)", + "async disconnect() -> None (base.py:109)", + "async drawing_info() -> DrawingInfo (base.py:113)", + "async drawing_new(template: str|None=None) -> dict (base.py:116)", + "async drawing_open(path: str) -> dict (base.py:119)", + "async drawing_save(path: str|None=None) -> dict (base.py:122)", + "async drawing_save_as(path: str, fmt: str='dwg') -> dict (base.py:125)", + "async drawing_export_dxf(path: str) -> dict (base.py:128)", + "async drawing_export_pdf(path: str) -> dict (base.py:131)", + "async drawing_purge() -> dict (base.py:134)", + "async drawing_audit() -> dict (base.py:137)", + "async drawing_close(save: bool=True) -> dict (base.py:140)", + "async drawing_undo() -> dict (base.py:143)", + "async drawing_redo() -> dict (base.py:146)", + "async entity_create_line(x1,y1,x2,y2,z1=0,z2=0,layer=None,color=None,linetype=None) -> EntityInfo (base.py:150)", + "async entity_create_circle(cx,cy,radius,layer=None,color=None) -> EntityInfo (base.py:158)", + "async entity_create_arc(cx,cy,radius,start_angle,end_angle,layer=None,color=None) -> EntityInfo (base.py:164)", + "async entity_create_polyline(points,closed=False,layer=None,color=None) -> EntityInfo (base.py:171)", + "async entity_create_text(text,x,y,height=2.5,rotation=0,layer=None,color=None) -> EntityInfo (base.py:177)", + "async entity_create_mtext(text,x,y,width=100,height=2.5,layer=None,color=None) -> EntityInfo (base.py:184)", + "async entity_create_hatch(pattern,boundary_points,scale=1,angle=0,layer=None,color=None) -> EntityInfo (base.py:191)", + "async entity_create_spline(fit_points,layer=None,color=None) -> EntityInfo (base.py:198)", + "async entity_create_ellipse(cx,cy,major_x,major_y,ratio=0.5,layer=None,color=None) -> EntityInfo (base.py:204)", + "async entity_create_point(x,y,layer=None,color=None) -> EntityInfo (base.py:211)", + "async entity_create_block_ref(name,x,y,scale_x=1,scale_y=1,rotation=0,layer=None) -> EntityInfo (base.py:217)", + "async dimension_linear(x1,y1,x2,y2,dim_x,dim_y,rotation=0,layer=None) -> EntityInfo (base.py:225)", + "async dimension_aligned(x1,y1,x2,y2,dim_x,dim_y,layer=None) -> EntityInfo (base.py:232)", + "async dimension_angular(vx,vy,x1,y1,x2,y2,tx,ty,layer=None) -> EntityInfo (base.py:239)", + "async dimension_radius(cx,cy,chord_x,chord_y,leader_length=10,layer=None) -> EntityInfo (base.py:247)", + "async dimension_diameter(x1,y1,x2,y2,leader_length=10,layer=None) -> EntityInfo (base.py:253)", + "async entity_move(handle,dx,dy,dz=0) -> dict (base.py:260)", + "async entity_copy(handle,dx,dy,dz=0) -> EntityInfo (base.py:263)", + "async entity_rotate(handle,base_x,base_y,angle_deg) -> dict (base.py:266)", + "async entity_scale(handle,base_x,base_y,factor) -> dict (base.py:271)", + "async entity_mirror(handle,x1,y1,x2,y2,delete_original=False) -> EntityInfo (base.py:276)", + "async entity_offset(handle,distance,side_x=None,side_y=None) -> EntityInfo (base.py:283)", + "async entity_delete(handle) -> dict (base.py:289)", + "async entity_array_rectangular(handle,rows,cols,row_spacing,col_spacing) -> list[EntityInfo] (base.py:292)", + "async entity_array_polar(handle,count,fill_angle,center_x,center_y) -> list[EntityInfo] (base.py:298)", + "async entity_trim(target_handle,cutter_handle,keep_x,keep_y) -> EntityInfo (base.py:306)", + "async entity_extend(target_handle,boundary_handle,end_x=None,end_y=None) -> EntityInfo (base.py:315)", + "async entity_fillet(handle1,handle2,radius,trim=True) -> EntityInfo (base.py:324)", + "async entity_chamfer(handle1,handle2,dist1,dist2=None,trim=True) -> EntityInfo (base.py:334)", + "async entity_get(handle) -> EntityInfo (base.py:345)", + "async entity_set_properties(handle,layer=None,color=None,linetype=None,lineweight=None,visible=None) -> dict (base.py:348)", + "async entity_list(type_filter=None,layer_filter=None,limit=200,offset=0) -> list[EntityInfo] (base.py:358)", + "async layer_list() -> list[LayerInfo] (base.py:368)", + "async layer_create(name,color=7,linetype='Continuous',lineweight=-3) -> LayerInfo (base.py:371)", + "async layer_delete(name) -> dict (base.py:379)", + "async layer_set_current(name) -> dict (base.py:382)", + "async layer_modify(name,color=None,linetype=None,lineweight=None) -> LayerInfo (base.py:385)", + "async layer_freeze/thaw/lock/unlock/hide/show(name) -> dict (base.py:393-409)", + "async linetype_list() -> list[str] (base.py:412)", + "async linetype_load(name,file=None) -> dict (base.py:415)", + "async block_list() -> list[BlockInfo] (base.py:421)", + "async block_insert(name,x,y,scale_x=1,scale_y=1,rotation=0,attributes=None,layer=None) -> EntityInfo (base.py:424)", + "async block_explode(handle) -> dict (base.py:432)", + "async block_get_attributes(handle) -> dict (base.py:435)", + "async block_set_attributes(handle,attributes) -> dict (base.py:438)", + "async block_create_from_entities(name,handles,base_x=0,base_y=0) -> dict (base.py:441)", + "async analysis_stats() -> dict (base.py:448)", + "async analysis_entities_in_region(x1,y1,x2,y2) -> list[EntityInfo] (base.py:451)", + "async analysis_measure_distance(x1,y1,x2,y2) -> float (base.py:456)", + "async analysis_measure_area(points) -> float (base.py:461)", + "async analysis_bounding_box() -> dict (base.py:464)", + "async analysis_select_by_layer(layer_name) -> list[EntityInfo] (base.py:467)", + "async analysis_select_by_type(entity_type) -> list[EntityInfo] (base.py:470)", + "async view_zoom_extents() -> dict (base.py:474)", + "async view_zoom_window(x1,y1,x2,y2) -> dict (base.py:477)", + "async view_screenshot() -> bytes|None (base.py:482)", + "async transaction_begin/commit/rollback() -> dict (base.py:486-493)", + "async system_status() -> dict (base.py:496)", + "async system_get_variable(name) -> Any (base.py:499)", + "async system_set_variable(name,value) -> dict (base.py:502)", + "async system_run_command(command) -> dict (base.py:505)", + "async system_run_lisp(expression) -> dict (base.py:508)", + "async drawing_plan(intent,sheet_size='A3',scale=1,layer_set_id='mech',view_count=1,dim_style='chain',notes=None) -> PlanSpec (base.py:512)", + "async drawing_critique(focus=None) -> list[Issue] (base.py:526)", + "async point_from_snap(handle,snap,ref_x=None,ref_y=None) -> tuple[float,float] (base.py:534)", + "async construction_xline(x,y,angle_deg,layer='CONSTRUCTION') -> EntityInfo (base.py:545)", + "async construction_clear(layer='CONSTRUCTION') -> dict (base.py:554)", + "async drawing_apply_iso_layers(standard='mech') -> dict (base.py:562)", + "async dimension_auto(handles,style='chain',offset=10) -> list[EntityInfo] (base.py:570)", + "async entity_select_smart(predicate: dict) -> list[EntityInfo] (base.py:580)", + "CONCRETE set_layer_active(name) -> None (base.py:594)", + "CONCRETE ensure_linetypes(names,file=None) -> dict[str,str] (base.py:598)", + "free fn shoelace_area(points) -> float (base.py:626); deg2rad (base.py:639); rad2deg (base.py:643)" + ], + "dependencies": [ + "abc.ABC/abstractmethod", + "dataclasses.dataclass/field", + "math", + "typing.Any/TYPE_CHECKING", + "engineering.plan_spec (TYPE_CHECKING-only: CritiqueFocus, DimStyle, Issue, LayerSetId, PlanSpec, SheetSize, SnapType) — string-annotated, runtime-imported inside ezdxf_backend methods" + ], + "key_flows": [ + "server.py::_make_backend instantiates EzdxfBackend or ComBackend (both subclass AutoCADBackend); the singleton is stored in lifespan context and every @mcp.tool calls _backend(ctx).(...).", + "Dataclass returns (EntityInfo/LayerInfo/BlockInfo/DrawingInfo) flow back through server.py::_dc() which converts them to dicts for JSON; CommandResult has its own to_dict().", + "Premium meta-tools (drawing_plan/critique/point_from_snap/construction_*/drawing_apply_iso_layers/dimension_auto/entity_select_smart) are part of the contract and resolve engineering.plan_spec / engineering.critique at call time inside the ezdxf implementation.", + "ensure_linetypes (concrete) drives backend-specific linetype_list/linetype_load via the contract methods; used by the engineering scaffold bootstrap." + ], + "risks": [ + { + "title": "COM backend stubs ALL 8 premium meta-tools with NotImplementedError — contract violation", + "severity": "critical", + "location": "backends/com_backend.py:1637-1660", + "detail": "drawing_plan, drawing_critique, point_from_snap, construction_xline, construction_clear, drawing_apply_iso_layers, dimension_auto, and entity_select_smart each `raise NotImplementedError(\"... pending Task #7/#8/#9\")`. EzdxfBackend fully implements all 8 (ezdxf_backend.py:1753-2060). CLAUDE.md declares the premium workflow 'non-negotiable for production engineering output', yet on Windows the default backend is COM (server._make_backend tries COM first). Result: drawing_plan/drawing_apply_iso_layers/drawing_critique/dimension_auto and the mandated finalize gate are dead on the very platform the project targets — every premium-workflow tool call crashes with NotImplementedError surfacing as a 500/ToolError. This is the single largest contract-vs-implementation divergence in the repo." + }, + { + "title": "drawing_save_as default `fmt` differs from the contract (dwg vs dxf)", + "severity": "medium", + "location": "backends/ezdxf_backend.py:345 vs backends/base.py:126", + "detail": "ABC signature is `drawing_save_as(self, path: str, fmt: str = 'dwg')` (base.py:126); ComBackend matches with `fmt='dwg'` (com_backend.py:515) but EzdxfBackend declares `fmt: str = 'dxf'` (ezdxf_backend.py:345). A caller relying on the documented default gets DWG on COM and DXF on ezdxf for the same call — a silent cross-backend parity/format divergence. (Arguably ezdxf cannot produce real DWG, so the override is intentional, but it still breaks the contract default and is undocumented.)" + }, + { + "title": "Array tools: original-entity inclusion differs between backends (count parity)", + "severity": "medium", + "location": "backends/ezdxf_backend.py:925-934 vs backends/com_backend.py:893-904", + "detail": "EzdxfBackend.entity_array_rectangular explicitly `continue`s the (0,0) cell (ezdxf_backend.py:927-928) and entity_array_polar iterates `range(1,count)` (ezdxf_backend.py:947), so it returns rows*cols-1 / count-1 NEW entities (original excluded). The COM path calls native ArrayRectangular/ArrayPolar (com_backend.py:893,920) and returns whatever COM yields, with an except-branch that, on iteration failure, appends a single `_entity_info(result)` (com_backend.py:903,929). The returned list length and whether the original is represented are not guaranteed equal across backends for identical inputs — clients counting/positioning by list index can break when switching backends." + }, + { + "title": "entity_extend auto-endpoint fallback can extend toward origin on COM", + "severity": "low", + "location": "backends/com_backend.py:1568-1583", + "detail": "When end_x/end_y are None and reading StartPoint/EndPoint raises, the except clause sets `ex, ey = 0.0, 0.0` (com_backend.py:1580-1581) and then issues `_EXTEND ... 0,0`, picking the drawing origin as the pick point. For geometry far from origin this extends/selects the wrong segment silently instead of failing. The contract (base.py:319-322) promises 'the target endpoint nearest the boundary is auto-selected' — the silent (0,0) fallback violates that promise." + }, + { + "title": "point_from_snap 'int' (intersection) snap declared in contract but support unverified per type", + "severity": "low", + "location": "backends/base.py:540-543 vs backends/ezdxf_backend.py:1775", + "detail": "The docstring enumerates snap ∈ {end, mid, center, quad, int, perp, near} (base.py:540) and SnapType is a closed type. The ezdxf _sync handler (ezdxf_backend.py:1778+) branches on snap strings; I read through line 1812 (end/arc handling) but did not confirm every enum member — notably 'int' (intersection, which needs a second entity but the signature only takes one handle + ref_x/ref_y). If 'int' falls through to a generic else it may raise or mis-snap. Verify the full branch list covers all 7 SnapType members and that 'int'/'perp' semantics match the single-handle signature." + } + ], + "smells": [ + { + "title": "BlockInfo.description field is never populated by either backend", + "severity": "low", + "location": "backends/base.py:71; backends/ezdxf_backend.py:1195-1201; backends/com_backend.py:1185-1191", + "detail": "BlockInfo declares `description: str = ''` (base.py:71) but neither block_list constructs BlockInfo with a description — ezdxf (ezdxf_backend.py:1195) and COM (com_backend.py:1185) both omit the kwarg, so it is always ''. ezdxf block definitions expose `blk.block.dxf.description`; the field is dead/unused as currently wired." + }, + { + "title": "Premium meta-tool stubs reference internal task IDs in user-facing errors", + "severity": "low", + "location": "backends/com_backend.py:1639-1660", + "detail": "Each NotImplementedError message embeds 'pending Task #7/#8/#9' (e.g. com_backend.py:1639,1645,1653). These internal planning-ticket references leak into runtime error payloads surfaced to MCP clients; they convey nothing actionable to an end user and are doc-vs-code drift once the tasks are 'done' on the ezdxf side but not COM." + }, + { + "title": "drawing_redo / block_create_from_entities are contract-conformant but inert on respective backends", + "severity": "low", + "location": "backends/ezdxf_backend.py:486-487; backends/com_backend.py:1253-1260", + "detail": "EzdxfBackend.drawing_redo always returns {'ok': False, 'error': 'Redo not supported in ezdxf backend'} (ezdxf_backend.py:487) and ComBackend.block_create_from_entities always returns {'ok': False, ...'not supported in COM backend'...} (com_backend.py:1256-1260). These satisfy the abstract signature shape but are permanently no-op, creating asymmetric capability across backends not flagged in base.py — silent feature gaps a caller can only discover at runtime." + }, + { + "title": "_v2 nested helper redefined per-call inside _entity_info_dxf", + "severity": "low", + "location": "backends/ezdxf_backend.py:122-124", + "detail": "`_v2` (Vec3/tuple -> [x,y]) is defined as a closure inside _entity_info_dxf on every entity conversion (ezdxf_backend.py:122). Minor allocation/readability smell — it has no closure dependency and could be a module-level function reused across the file (COM has an analogous standalone pattern). Not a correctness issue." + }, + { + "title": "LayerInfo.lineweight default sentinel (-3) leaks raw ACAD enum into a float field", + "severity": "low", + "location": "backends/base.py:377; backends/ezdxf_backend.py:184-189", + "detail": "layer_create defaults lineweight=-3 (base.py:377) and _layer_info_dxf passes through `lineweight` defaulting to -3 (ezdxf_backend.py:184) as a float. -3 is AutoCAD's LWDEFAULT magic constant, not a millimetre value, but the dataclass field is typed `lineweight: float` (base.py:57) with no documentation that negative values are sentinels (-1 ByLayer, -2 ByBlock, -3 Default). Consumers doing arithmetic/ISO-128 validation on lineweight (the CLAUDE.md ISO-128 rule) can misread the sentinel as a real 0.x mm value." + } + ], + "notes": "Coverage cross-check: I grepped every `def`/`async def` in both backends (ezdxf_backend.py and com_backend.py) against the ~73 abstract methods. Both backends define a method body for EVERY abstract method (no missing-override AttributeError risk), so all subclasses instantiate. The contract violations are behavioral, not structural: COM stubs the 8 premium meta-tools with NotImplementedError (critical, since COM is the default Windows backend), and several methods are inert no-ops (drawing_redo on ezdxf, block_create_from_entities on COM). I verified dimension_* (5), corner ops (trim/extend/fillet/chamfer), arrays, block_list, and the dataclass constructors directly. NOT fully read: the complete point_from_snap branch table beyond line 1812, engineering/plan_spec.py, engineering/critique.py, and server.py tool registrations — the 'int'/perp snap risk and dimension_auto internals should be re-verified against those files in a follow-up pass." + }, + { + "module": "backends/ezdxf_backend.py — EzdxfBackend (headless DXF backend)", + "purpose": "Headless, file-based implementation of the AutoCADBackend ABC using the ezdxf library. Provides DXF create/modify/query/analysis/dimension/block/layer/linetype operations plus the \"premium\" meta-tools (plan/critique/snap/construction/iso-layers/auto-dim/smart-select) and corner ops (trim/extend/fillet/chamfer). All synchronous ezdxf work is offloaded to threads via _async(); transactions are implemented as full on-disk DXF snapshots pushed onto an _undo_stack. Runs anywhere (no AutoCAD/COM required); view zoom and system run-command/run-lisp are documented no-ops.", + "public_surface": [ + "EzdxfBackend(AutoCADBackend) — full backend implementation", + "connect/disconnect, _async(func) thread+lock wrapper (250-252)", + "drawing_* : info/new/open/save/save_as/export_dxf/export_pdf/purge/audit/close/undo/redo (256-487)", + "entity_create_* : line/circle/arc/polyline/text/mtext/hatch/spline/ellipse/point/block_ref (508-664)", + "dimension_* : linear/aligned/angular/radius/diameter + premium dimension_auto (668-774, 1926-2006)", + "entity modify: move/copy/rotate/scale/mirror/offset/delete/array_rectangular/array_polar (785-960)", + "entity query: get/set_properties/list (964-1011)", + "layer_* (12 ops) + linetype_list/load (1015-1179)", + "block_* : list/insert/explode/get_attributes/set_attributes/create_from_entities (1183-1289)", + "analysis_* : stats/entities_in_region/measure_distance/measure_area/bounding_box/select_by_layer/select_by_type (1293-1390)", + "view_zoom_extents/window (no-op), view_screenshot (matplotlib PNG) (1394-1424)", + "transaction_begin/commit/rollback (1428-1476)", + "system_status/get_variable/set_variable/run_command(no-op)/run_lisp(no-op) (1480-1520)", + "corner ops: entity_trim/extend/fillet/chamfer (1535-1750)", + "premium: drawing_plan/critique, point_from_snap, construction_xline/clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart (1753-2050)", + "module helpers: _entity_info_dxf, _layer_info_dxf, _ensure_linetype_loaded, _add_linetype_from_fallback (53-194)" + ], + "dependencies": [ + "ezdxf (core: new/readfile, layouts, math.Matrix44/Vec2/Vec3, bbox, tools.standards, addons.drawing matplotlib Frontend/RenderContext)", + "matplotlib (optional: view_screenshot, drawing_export_pdf)", + "config.settings (max_dxf_bytes, max_undo_stack)", + "backends.base (AutoCADBackend ABC, EntityInfo/LayerInfo/BlockInfo/DrawingInfo dataclasses, shoelace_area)", + "engineering.plan_spec.PlanSpec, engineering.critique.run_critique, engineering.layers.apply_layer_set (lazy-imported in premium meta-tools)", + "stdlib: asyncio, io, math, os, tempfile, pathlib" + ], + "key_flows": [ + "Create: tool -> entity_create_* -> _msp().add_* -> _apply_attrs(layer/color/linetype) -> _mark_dirty() -> _entity_info_dxf() returns handle; wrapped by _async (lock + to_thread).", + "Modify/query: handle -> _get_entity(handle) via doc.entitydb.get(handle) -> mutate via .transform()/.translate() or read .dxf.* attrs.", + "Transactions/undo: transaction_begin saves full doc.saveas(tmpfile) onto _undo_stack (trimmed to max_undo_stack); rollback/undo do ezdxf.readfile(snapshot) replacing self._doc; commit just unlinks top snapshot.", + "Linetype resolution: _ensure_linetype_loaded -> ezdxf.tools.standards -> _AUTOCAD_FALLBACK_LINETYPES -> warn-and-continue (renders Continuous).", + "Screenshot/PDF: build matplotlib figure via ezdxf addons.drawing Frontend/RenderContext, savefig to BytesIO/path; ImportError -> None (screenshot) or RuntimeError (pdf).", + "Premium auto-dim: dimension_auto gathers LINE endpoints then emits dimension_linear calls per chain/baseline/ordinate style on layer DIM." + ], + "risks": [ + { + "title": "drawing_undo / transaction_rollback reload doc but do NOT reset _doc_path or _current_layer", + "severity": "medium", + "location": "backends/ezdxf_backend.py:470-484, 1462-1476", + "detail": "Both replace self._doc via ezdxf.readfile(snapshot) but leave self._doc_path and self._current_layer unchanged, and force self._dirty=True. If the snapshot was taken before a drawing_save_as that changed _doc_path, the in-memory path now disagrees with the reloaded content. More importantly drawing_undo/rollback share the SAME _undo_stack with transaction_begin: a user-level transaction snapshot and an undo checkpoint are indistinguishable, so transaction_commit/rollback can silently pop a snapshot created for a different purpose (no transaction-id or depth separation)." + }, + { + "title": "entity_offset ignores side_x/side_y; offset side is not controllable and ARC/LWPOLYLINE/ELLIPSE unsupported", + "severity": "medium", + "location": "backends/ezdxf_backend.py:868-908", + "detail": "The signature accepts side_x/side_y but they are never read. LINE offset direction is hard-fixed to normal=(-dir.y, dir.x)*distance, so the caller cannot choose which side; only the sign of `distance` flips it. CIRCLE offset is radius+distance (no side either). All other types (ARC, LWPOLYLINE, ELLIPSE, SPLINE) raise RuntimeError('Offset not supported'), i.e. silently-unsupported for the common polyline-offset case." + }, + { + "title": "dimension_angular ignores text-location params tx,ty and uses hardcoded distance=10.0", + "severity": "medium", + "location": "backends/ezdxf_backend.py:705-722", + "detail": "Signature is (vx,vy,x1,y1,x2,y2,tx,ty,layer) but tx/ty are never used; add_angular_dim_2l is called with a fixed distance=10.0 regardless of the requested text/arc location. The dimension arc radius is therefore always 10 drawing-units from the vertex, which for large or small parts places the arc inside/outside the geometry. COM parity is broken (COM presumably honors tx/ty)." + }, + { + "title": "entity_array_polar double-places at full 360deg fill (overlap with original)", + "severity": "medium", + "location": "backends/ezdxf_backend.py:946-947", + "detail": "step = radians(fill_angle)/max(count-1,1) and the loop runs i in range(1,count). For a full circle fill_angle=360 the inclusive-endpoint divisor count-1 makes the last copy land at exactly 360deg == 0deg, coinciding with the original entity (duplicate). AutoCAD's polar array divides a 360 fill by count (not count-1). For partial fills count-1 is correct, so the bug is specifically the closed/full-circle case — a very common gear/bolt-circle pattern, which the engineering layer relies on." + }, + { + "title": "entity_copy/mirror/array add a raw .copy() to modelspace, relying on add_entity to assign a fresh handle", + "severity": "low", + "location": "backends/ezdxf_backend.py:793-801, 834-866, 918-960", + "detail": "ent.copy() in ezdxf produces an unbound entity that still carries the source handle until add_entity rebinds it. The code calls add_entity(copy) BEFORE reading copy.dxf.handle (in array/explode) and before translate, which is the correct order — but it is fragile: any reordering, or an ezdxf version where copy() retains the handle and add_entity does not reassign, would yield duplicate handles silently. No assertion that the new handle differs from the source." + }, + { + "title": "Broad except Exception: pass swallows failures in purge and block_create_from_entities", + "severity": "low", + "location": "backends/ezdxf_backend.py:397-398, 410-412, 424-428, 438-442, 1285-1286", + "detail": "drawing_purge silently swallows every delete failure (blocks/layers/linetypes/styles) with bare 'except Exception: pass', so a layer that fails to purge is counted as not-purged with no diagnostic. block_create_from_entities swallows per-handle add failures at debug level, so a block can be created reporting entity_count lower than requested with no error surfaced to the caller." + }, + { + "title": "view_screenshot/export_pdf catch only ImportError; runtime render errors propagate or are inconsistent", + "severity": "low", + "location": "backends/ezdxf_backend.py:357-379, 1400-1424", + "detail": "Both wrap the matplotlib render in try/except ImportError only. A non-import rendering failure (e.g. unsupported entity, empty modelspace, savefig OSError) propagates as a raw exception out of the threaded _sync for view_screenshot (which is documented to return bytes|None), and for export_pdf too. Also fig is not closed on the non-Import exception path, leaking matplotlib figures across repeated failing calls." + }, + { + "title": "_apply_attrs auto-creates layers but does NOT ensure the entity's linetype is loaded", + "severity": "low", + "location": "backends/ezdxf_backend.py:491-501", + "detail": "entity_create_* -> _apply_attrs sets entity.dxf.linetype = linetype directly without calling _ensure_linetype_loaded (unlike entity_set_properties:980 and layer_modify:1074 which do). Creating a line with linetype='HIDDEN' assigns the name but never adds the linetype table entry, so ezdxf may raise on save or the line renders Continuous. Inconsistent with the documented 'write linetype=\"CENTER\" without separately loading it' contract." + }, + { + "title": "drawing_save_as ignores fmt parameter (always writes DXF via saveas)", + "severity": "low", + "location": "backends/ezdxf_backend.py:345-352", + "detail": "fmt is accepted and echoed back in the result but doc.saveas(path) only ever writes the format inferred from extension/DXF. A caller passing fmt='dwg' gets {'format':'dwg'} but a DXF file on disk — silent format mismatch. ezdxf cannot write DWG; this should error rather than claim success." + }, + { + "title": "Concurrency: every op serialized on a single asyncio.Lock; commit does file I/O outside the lock", + "severity": "low", + "location": "backends/ezdxf_backend.py:250-252, 1452-1460", + "detail": "_async holds self._lock around the to_thread call, so all backend operations are fully serialized (no parallelism — acceptable for a single doc but defeats the thread pool). transaction_commit runs p.unlink() directly on the event loop thread WITHOUT _async/lock, racing with any concurrent _async op that touches _undo_stack (e.g. transaction_begin trimming the stack). The list mutation of _undo_stack is not lock-protected in commit." + }, + { + "title": "block_list base_point/origin indexing assumes 3-tuple; entities_in_region fallback uses only insert/start", + "severity": "low", + "location": "backends/ezdxf_backend.py:1193-1200, 1330-1343", + "detail": "block_list reads origin[0]/origin[1] from dxf.get('base_point', default) — fine for the default but if base_point is a Vec3 subscripting may differ across ezdxf versions. analysis_entities_in_region's non-bbox fallback only inspects insert/start points, so circles/arcs/text-by-center and any entity without .insert/.start are silently excluded from region selection when _BBOX_OK is False." + } + ], + "smells": [ + { + "title": "V1 placeholder limitations advertised in error strings (LINE-only trim/extend/fillet/chamfer/dimension_auto)", + "severity": "medium", + "location": "backends/ezdxf_backend.py:1542-1543, 1581-1583, 1623-1625, 1941-1943, 1878-1882", + "detail": "trim/extend/fillet/chamfer all raise 'V1 supports LINE+LINE only ... V2 will add LINE+ARC'; dimension_auto raises 'V1: only LINE entities supported'; point_from_snap 'int' raises 'V2 will accept a 2nd handle'. These are functional gaps shipped as runtime errors. ARC/CIRCLE/LWPOLYLINE corner ops — core CAD operations — are unimplemented despite the premium workflow docs implying fillet/chamfer are first-class." + }, + { + "title": "Unused imports / accepted-but-ignored parameters", + "severity": "low", + "location": "backends/ezdxf_backend.py:34-36, 705-722, 868-908, 1614-1656", + "detail": "colors, units, TextEntityAlignment, BSpline, Vec3 imported with noqa but unused in this module. _fillet_chamfer_setup returns A_far/B_far which entity_fillet (1660) and entity_chamfer (1723) unpack but never use. dimension_angular tx/ty and entity_offset side_x/side_y are dead parameters (see risks)." + }, + { + "title": "entity_create_mtext: rotation cannot be set; width/height defaults baked into module not config", + "severity": "low", + "location": "backends/ezdxf_backend.py:578-591", + "detail": "MTEXT has no rotation parameter (TEXT does at 560-576), so rotated multi-line notes are impossible via this backend. The COM backend may support rotation, creating cross-backend behavior divergence for the same MCP tool." + }, + { + "title": "Duplicated handle-nearest-endpoint trim logic copy-pasted across fillet, chamfer, trim, extend", + "severity": "low", + "location": "backends/ezdxf_backend.py:1564-1567, 1700-1711, 1737-1747", + "detail": "The 'if _dist2(start,P) <= _dist2(end,P): set start else set end' block is duplicated 6+ times across entity_trim, entity_fillet, entity_chamfer. Should be a shared helper (e.g. _shorten_line_to(line, P, new_pt)) to avoid divergent edits." + }, + { + "title": "drawing_undo and transaction_rollback are near-identical duplicate implementations", + "severity": "low", + "location": "backends/ezdxf_backend.py:470-484, 1462-1476", + "detail": "Both pop _undo_stack, ezdxf.readfile the snapshot into self._doc, set _dirty=True, and unlink the temp file. Same body, two methods sharing the same stack — confirms the conceptual conflation of 'undo history' and 'transactions' noted in risks. Should delegate to one private helper." + }, + { + "title": "Magic constants: angular dim distance=10.0, screenshot figsize 16x9 dpi 100, pdf dpi 150, construction color 250", + "severity": "low", + "location": "backends/ezdxf_backend.py:714, 372, 1410-1417, 1897", + "detail": "Hardcoded render/geometry constants not surfaced through config.settings. The angular-dim 10.0 is a correctness issue (see risk); the dpi/figsize/color are tuning knobs that differ from the COM backend's capture path and produce non-matching screenshots between backends." + }, + { + "title": "entity_create_text rotation passed straight through; no angle normalization vs AutoCAD CCW-from-X convention", + "severity": "low", + "location": "backends/ezdxf_backend.py:560-576", + "detail": "rotation is forwarded as-is to dxfattribs['rotation']. ezdxf TEXT rotation is degrees CCW which matches the documented convention, so this is OK, but there is no validation/normalization and _entity_info_dxf reads rotation back via ent.dxf.get('rotation',0.0) — consistent, just noting no guard for radians-vs-degrees caller mistakes." + }, + { + "title": "system_set_variable writes arbitrary header vars with no validation/coercion", + "severity": "low", + "location": "backends/ezdxf_backend.py:1502-1508", + "detail": "doc.header[f'${name.upper()}'] = value with no type check; passing a string '4' for $INSUNITS (which drawing_info reads as an int key into unit_map) would store a string and break the unit_map.get lookup. The CLAUDE.md prompt even shows users calling system_set_variable('INSUNITS', 4) — a wrong-typed value silently corrupts the header var." + } + ], + "notes": "Read the full file (lines 1-2050) plus config.py (max_undo_stack=5, max_dxf_bytes=50MB) and base.py signatures, and confirmed in server.py that engineering-layer bootstrap lives in the server tool (drawing_new in this backend correctly does NOT bootstrap, so no doc drift there). _async() correctness: it acquires self._lock then asyncio.to_thread — fully serializing the single doc (intended for STA-like safety, but defeats the thread pool's purpose since only one op runs at a time). Transaction model is full-DXF-snapshot-to-tempfile on _undo_stack, trimmed to max_undo_stack=5; this means every transaction_begin does a complete document serialization to disk — O(drawing size) per call and a real perf/memory concern for large drawings (the snapshots also persist on disk between calls). Documented no-ops confirmed: view_zoom_extents/window (1394-1398) and system_run_command/run_lisp (1510-1520) return ok/error dicts without doing work, matching CLAUDE.md. Silently-unsupported entity types found: entity_offset (only LINE/CIRCLE), corner ops + dimension_auto + point_from_snap-int (LINE-only), entities_in_region fallback (insert/start only). The most impactful correctness bugs to verify with tests: (1) entity_array_polar full-360 duplicate, (2) dimension_angular ignoring tx/ty + fixed distance, (3) entity_offset ignoring requested side, (4) _apply_attrs not loading linetypes on create." + }, + { + "module": "backends/com_backend.py — ComBackend (live AutoCAD COM control)", + "purpose": "Implements the AutoCADBackend ABC against a live AutoCAD instance via pywin32 COM. All COM access is serialized through a single-thread STA ThreadPoolExecutor (one worker, CoInitialize'd) wrapped by async def _run, which applies a hard per-call timeout (config.settings.com_call_timeout, default 60s) and rebuilds the executor + drops the cached app on timeout/fatal HRESULT. Provides entity create/modify/query, layers, linetypes, blocks, dimensions, analysis (UUID-named SelectionSets), Win32 PrintWindow screenshots, transactions (AutoCAD undo marks), and SendCommand-based run_command/run_lisp + corner ops (trim/extend/fillet/chamfer via _safe_send_command polling CMDACTIVE).", + "public_surface": [ + "class ComBackend(AutoCADBackend)", + "ComBackend._run(func,*args) — STA executor dispatch + timeout/HRESULT recovery (com_backend.py:397)", + "module _acad_app/_acad_doc/_msp — lazy COM app/doc/modelspace (com_backend.py:98,117,125)", + "module _capture_window(hwnd) — GDI PrintWindow PNG capture (com_backend.py:303)", + "module _safe_send_command(doc,cmd,deadline_s) static — CMDACTIVE-polling command runner (com_backend.py:1497)", + "module _ensure_linetype_loaded / _apply_entity_attrs / _entity_info / _layer_info helpers", + "entity_create_* / entity_* modify / dimension_* / layer_* / block_* / analysis_* / view_* / transaction_* / system_* tool methods", + "8 premium meta-tools (drawing_plan, drawing_critique, point_from_snap, construction_xline, construction_clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart) — all raise NotImplementedError (com_backend.py:1637-1660)" + ], + "dependencies": [ + "pywin32 (pythoncom, pywintypes, win32com.client, win32con, win32gui, win32ui)", + "ctypes (PrintWindow)", + "Pillow / PIL.Image (screenshot encode)", + "config (config.settings.com_call_timeout)", + "backends.base (AutoCADBackend ABC, EntityInfo/LayerInfo/BlockInfo/DrawingInfo, deg2rad/rad2deg/shoelace_area)", + "asyncio, concurrent.futures.ThreadPoolExecutor, uuid, math, io, time, logging" + ], + "key_flows": [ + "Async tool call → ComBackend.method() builds inner _sync() closure → await self._run(_sync) → loop.run_in_executor(single STA thread) → asyncio.wait_for(timeout) → COM call on worker thread → dataclass result", + "Connect: connect() only builds the ThreadPoolExecutor (initializer=_com_init→CoInitialize); actual AutoCAD GetActiveObject/Dispatch is lazy on first _acad_app() call inside the worker thread", + "Timeout recovery: asyncio.TimeoutError → abandon stuck executor via shutdown(wait=False), build fresh single-thread executor, pop cached _COM_STATE['app'], set _connected=False, raise RuntimeError", + "HRESULT recovery: pywintypes.com_error with hr in {RPC_E_DISCONNECTED, server-unavailable, busy} → drop cached app + _connected=False, re-raise RuntimeError", + "Selection-set flow (analysis_entities_in_region / analysis_select_by_layer): SelectionSets.Add(UUID name) → select → iterate → finally ss.Delete()", + "Corner ops: _safe_send_command zeros OSMODE/SNAPMODE/CMDECHO, snapshots ModelSpace handles, SendCommand, busy-polls CMDACTIVE with time.sleep(0.05) until 0 or deadline, restores vars, returns new handles", + "Screenshot: view_screenshot → _find_autocad_hwnd (EnumWindows title contains 'AutoCAD') → _capture_window GDI PrintWindow(PW_RENDERFULLCONTENT) → PIL BGRX→PNG" + ], + "risks": [ + { + "title": "Timeout abandons STA worker thread permanently while it stays blocked inside SendCommand, leaking a CoInitialize'd thread per timeout", + "severity": "high", + "location": "backends/com_backend.py:415-433", + "detail": "On asyncio.TimeoutError the code does stuck.shutdown(wait=False) and builds a fresh executor. The old worker thread is still blocked inside a synchronous COM/SendCommand call and cannot be cancelled by Python; shutdown(wait=False) does NOT kill it. Each timeout therefore permanently leaks one OS thread that holds an apartment (CoInitialize was called, CoUninitialize never is) plus a COM proxy to AutoCAD. Repeated timeouts (e.g. a modal dialog left open) accumulate zombie threads and COM references against the single AutoCAD process. There is no upper bound and no CoUninitialize anywhere in the file." + }, + { + "title": "_com_init never calls CoUninitialize; abandoned/replaced executor threads leak the COM apartment", + "severity": "medium", + "location": "backends/com_backend.py:68-71,386-390", + "detail": "_com_init calls pythoncom.CoInitialize() in the worker thread but no finalizer/atexit ever calls pythoncom.CoUninitialize(). disconnect() does executor.shutdown(wait=False) (does not block, does not run any per-thread teardown). Combined with the timeout-rebuild path (line 423), every replaced executor leaves a thread that initialized COM but never uninitialized it." + }, + { + "title": "_safe_send_command busy-polls CMDACTIVE with time.sleep on the single STA thread, monopolizing the executor for up to deadline_s and serializing all other tool calls", + "severity": "medium", + "location": "backends/com_backend.py:1521-1536", + "detail": "The poll loop runs entirely inside the one COM worker thread (called from entity_trim/extend/fillet/chamfer via _run). While it sleeps/polls for up to 8s, no other COM tool call can execute (single worker). Worse, the outer _run timeout (default 60s) is independent of deadline_s (8s); if a corner op's SendCommand wedges AutoCAD at a prompt, CMDACTIVE may never reach 0, the ESC×3 + RuntimeError fires at 8s — but if AutoCAD is fully hung the GetVariable('CMDACTIVE') call itself blocks and the 60s _run timeout triggers the executor-rebuild/zombie-thread path above." + }, + { + "title": "transaction_active flag left True if commit/rollback _run raises; rollback EndUndoMark+UNDO B can leave AutoCAD mid-command", + "severity": "medium", + "location": "backends/com_backend.py:1402,1411,1421,1414-1419", + "detail": "self._transaction_active is set to True only AFTER await self._run(_sync) succeeds in transaction_begin (line 1402), but if _run raises (timeout/COM error) the begin partially set the undo mark with no flag — and in commit/rollback the flag is cleared after _run; if _run raises, the flag stays stale True. transaction_rollback issues doc.EndUndoMark() then SendCommand('_UNDO B\\n') without the CMDACTIVE guard or _safe_send_command polling used elsewhere; if a command is active this UNDO can interleave badly. No state reconciliation on reconnect after timeout (which sets _connected=False but does not reset _transaction_active)." + }, + { + "title": "SelectionSets.Add(UUID) leaks if a name collides or selection set already exists from a prior crashed run; Add() failure precedes the try/finally", + "severity": "medium", + "location": "backends/com_backend.py:1291-1308,1336-1349", + "detail": "ss = doc.SelectionSets.Add(ss_name) is OUTSIDE the try block. UUID collision is improbable, but if a previous process aborted leaving an identically-named set, or if Add raises for any reason, no cleanup runs (acceptable since nothing was created). The more material issue: SelectionSets persist in the drawing for the session; the finally ss.Delete() only runs if Add succeeded. If _apoint/SelectCrossing raise (e.g. invalid coords), the finally still deletes, which is correct — but there is no SelectionSets pre-clean, so a leftover set from a hard crash (no finally executed) permanently blocks reuse of standard names and bloats the SelectionSets table until purge." + }, + { + "title": "_capture_window GDI cleanup can leak the bitmap/DC if SelectObject/CreateCompatibleDC partially fails or returns the old object", + "severity": "low", + "location": "backends/com_backend.py:307-349", + "detail": "Cleanup deletes save_bmp.GetHandle(), save_dc, mfc_dc, hwnd_dc. But save_dc.SelectObject(save_bmp) returns the previously-selected default bitmap which is never restored before DeleteDC; selecting a bitmap into a DC and deleting the DC while the bitmap is still selected is a known GDI leak pattern on some drivers. Also if win32ui.CreateDCFromHandle succeeds but CreateCompatibleDC raises, hwnd_dc is released but mfc_dc.DeleteDC is still attempted (None-guarded, fine) — main concern is the unrestored default bitmap." + }, + { + "title": "_find_autocad_hwnd matches any visible window whose title contains 'AutoCAD', may capture a wrong/splash/secondary window", + "severity": "low", + "location": "backends/com_backend.py:283-300", + "detail": "EnumWindows returns the first visible window with 'AutoCAD' in the title and stops. This can match a tool palette, About dialog, splash, or a different AutoCAD-branded product, producing a screenshot of the wrong window. It is not tied to the COM ActiveDocument/app HWND (AcadApplication.HWND is available and would be authoritative)." + }, + { + "title": "system_run_lisp returns the SendCommand return value as 'result' but AutoCAD SendCommand returns None (no LISP value capture)", + "severity": "low", + "location": "backends/com_backend.py:1487-1492", + "detail": "doc.SendCommand always returns None in the AutoCAD COM API; it does not return the LISP evaluation result. The method reports result='nil' always (str(None) is falsy→'nil'), so callers get no actual LISP return value. This is a silent correctness/UX gap: the tool advertises a 'result' that is never meaningful. No CMDACTIVE guard either, so a LISP form that opens a prompt deadlocks the next call." + }, + { + "title": "entity_offset side_x/side_y parameters are accepted but ignored; offset direction is non-deterministic", + "severity": "low", + "location": "backends/com_backend.py:860-874", + "detail": "The signature takes side_x/side_y to pick which side to offset, but the body only calls ent.Offset(float(distance)) and never uses side params. AutoCAD's Offset with a signed distance offsets to a fixed side per entity orientation; positive/negative cannot reliably honor the caller's intended side. Result side is effectively unpredictable vs the documented intent, and diverges from any ezdxf implementation that may honor side." + }, + { + "title": "drawing_audit/transaction_rollback/drawing_undo/redo use SendCommand without the CMDACTIVE pre-guard used in system_run_command", + "severity": "low", + "location": "backends/com_backend.py:543-571,1414-1419", + "detail": "system_run_command added a CMDACTIVE check (line 1473) precisely to avoid deadlocking when a command/prompt is active, but drawing_audit ('_AUDIT Y'), drawing_undo ('_UNDO 1'), drawing_redo ('_REDO'), and transaction_rollback ('_UNDO B') call doc.SendCommand directly with no such guard and no _safe_send_command polling. If AutoCAD is at a prompt, these silently misfire or leave the command line in a bad state, and the next COM call can hang until the _run timeout." + } + ], + "smells": [ + { + "title": "All 8 premium meta-tools are NotImplementedError in COM backend but fully implemented in ezdxf — hard parity gap that breaks the documented premium workflow on Windows/COM", + "severity": "high", + "location": "backends/com_backend.py:1636-1660", + "detail": "drawing_plan, drawing_critique, point_from_snap, construction_xline, construction_clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart all raise NotImplementedError ('pending Task #7/#8/#9'). CLAUDE.md mandates these as 'non-negotiable for production engineering output' (plan-before-draw, point_from_snap, drawing_critique-then-finalize, drawing_apply_iso_layers). ezdxf_backend.py implements all of them (lines 1753-2008 verified). On the default Windows/COM backend the entire premium pipeline fails, and drawing_finalize→DrawingValidator runs against a backend that cannot critique. This is the single largest COM-vs-ezdxf parity defect." + }, + { + "title": "_batch_mode instance attribute is set/cleared but never read; only _COM_STATE['batch_mode'] is consulted by _regen", + "severity": "medium", + "location": "backends/com_backend.py:362,888-907,915-933,181", + "detail": "entity_array_rectangular/polar set both self._batch_mode and _COM_STATE['batch_mode'], but _regen() (line 181) only checks _COM_STATE.get('batch_mode'). self._batch_mode (initialized line 362) is dead — never read anywhere. Two parallel batch flags, one unused, invites drift. Also _COM_STATE is module-global mutable state shared across any ComBackend instance, conflated with the per-thread 'app' key." + }, + { + "title": "block_create_from_entities is a stub returning ok:False, diverging from the ABC contract and likely from ezdxf", + "severity": "medium", + "location": "backends/com_backend.py:1253-1260", + "detail": "Returns {'ok':False,'error':'... not supported in COM backend. Use system_run_command with _BLOCK instead.'} instead of implementing or raising. This is a silent capability gap vs base.py:441 abstract method; callers that branch on ok get a soft failure rather than a clear NotImplementedError, and parity with ezdxf is unverified/likely broken." + }, + { + "title": "Duplicated linetype-load logic between _ensure_linetype_loaded and linetype_load (MEASUREMENT/FILEDIA/-LINETYPE dance copy-pasted)", + "severity": "low", + "location": "backends/com_backend.py:133-166,1118-1169", + "detail": "The MEASUREMENT→acadiso.lin/acad.lin selection, FILEDIA save/restore, and '_-LINETYPE _LOAD ... \\n\\n' SendCommand sequence are duplicated almost verbatim. Divergence risk: linetype_load verifies post-load and raises on failure; _ensure_linetype_loaded does not verify, so setting entity.Linetype after a silent load failure can still throw a less helpful COM error." + }, + { + "title": "Repeated COM property re-reads of mspace.Count / Documents.Count inside hot loops and returns", + "severity": "low", + "location": "backends/com_backend.py:1269,1280,973-994,1358", + "detail": "analysis_stats reads mspace.Count in the for-range AND again in the return dict 'total_entities' (line 1280) — two COM round-trips that can disagree if the drawing changes; entity_list/analysis_select_by_type call mspace.Item(i) per index (O(n) COM calls, each marshaled across the STA boundary) with no caching. For large drawings this is slow and the double Count read is a latent inconsistency." + }, + { + "title": "Broad 'except Exception' with debug-only logging swallows COM errors throughout, masking real failures", + "severity": "low", + "location": "backends/com_backend.py:199,244,868,901,1216, and ~20 more", + "detail": "Many helpers (_entity_info type extraction, Offset indexing, ArrayRectangular iteration, GetAttributes, GetBoundingBox) catch bare Exception and log at debug level then continue/fallback, so genuine COM faults (disconnected, busy) are indistinguishable from benign 'property not supported' and never surface to the caller. The HRESULT-based reconnect logic in _run is bypassed because these inner catches eat the com_error before _run sees it." + }, + { + "title": "Stale/aspirational comments referencing 'Task #7/#8/#9' left in shipped code", + "severity": "low", + "location": "backends/com_backend.py:1636,1639,1642,1645,1648,1651,1654,1657", + "detail": "Section header '── premium meta-tools (Task #7/#8/#9 implement these) ──' and per-method 'pending Task #N' messages are TODO breadcrumbs that have shipped. They confirm the parity gap is known but unaddressed in this backend; doc (CLAUDE.md) vs code drift since CLAUDE.md presents the premium workflow as available." + } + ], + "notes": "Verified against backends/base.py (ABC + dataclasses), backends/ezdxf_backend.py (premium meta-tools implemented at lines 1753-2008, confirming the COM parity gap), config.py:38 (COM_CALL_TIMEOUT default 60s, env-overridable), and server.py:2281-2320 (drawing_finalize calls backend.view_screenshot + DrawingValidator().run(backend) + backend.drawing_info — finalize relies on COM methods that exist, but drawing_critique used as the production gate per CLAUDE.md does NOT exist on COM). Highest-impact items: (1) the timeout path leaks a CoInitialize'd STA worker thread + COM proxy every time it fires with no CoUninitialize anywhere (com_backend.py:415-433, 68-71); (2) the 8 NotImplementedError premium meta-tools make the documented mandatory workflow unusable on the default Windows/COM backend (com_backend.py:1636-1660). I did not execute anything (no AutoCAD/Windows COM available in this audit); risks are from static reading only." + }, + { + "module": "engineering/ package (AutoCAD MCP Pro deterministic CAD layer): gear.py, keyway.py, titleblock.py, critique.py, validator.py, layers.py, plan_spec.py, section.py, __init__.py", + "purpose": "Deterministic, parametric engineering-CAD primitives that back the gear_draw_*/keyway_draw_*/titleblock_apply_iso_a3/drawing_finalize/drawing_critique/drawing_plan MCP tools. It generates involute spur/helical gear outlines, DIN 6885 keyed bores and keyway sections, ISO 7200 A3 title blocks, standard engineering layer/linetype scaffolds, a closed-enum drawing critique, and an 8-step pre-completion validator. All drawing functions are async and call the abstract AutoCADBackend; pure-math helpers (involute/tooth) are sync.", + "public_surface": [ + "gear.py: involute_xy, generate_involute_flank, generate_tooth_profile, generate_full_gear_outline, draw_helical_gear_front_view, draw_spur_gear_front_view, draw_gear_section_aa, _draw_helix_symbol, _rotate_points, _arc_points", + "keyway.py: DIN6885_TABLE, keyway_dimensions, draw_keyed_bore, draw_keyway_section", + "titleblock.py: TitleBlockMetadata (dataclass), apply_iso_a3_titleblock", + "critique.py: run_critique + 6 focus checks (_check_iso128, _check_layer_color, _check_construction_left, _check_untrimmed_corner, _check_duplicate_entities, _check_dim_overlap)", + "validator.py: DrawingValidator.run, ValidationFinding, ValidationResult (8-step check)", + "layers.py: ENGINEERING_LAYERS, PID_LAYERS, ISO13567_LAYERS, LAYER_SET_REGISTRY, STANDARD_LINETYPES, ensure_standard_linetypes, ensure_engineering_layers, apply_layer_set", + "plan_spec.py: PlanSpec, Issue, SheetSize/LayerSetId/DimStyle/CritiqueFocus/SnapType literals, ALL_CRITIQUE_FOCUSES, ISO_128_LINEWEIGHTS_MM", + "section.py: cutting_plane_line, apply_section_hatch (NEITHER wired to any MCP tool)" + ], + "key_flows": [ + "MCP gear_draw_helical_front_view (server.py:2120) -> engineering.draw_helical_gear_front_view (gear.py:239) -> generate_full_gear_outline (gear.py:126) + draw_keyed_bore; gear_draw_spur_front_view (server.py:2157) delegates to helical with helix_angle=0", + "MCP gear_draw_section_aa (server.py:2183) -> draw_gear_section_aa (gear.py:358) -> draw_keyway_section (keyway.py:112)", + "MCP keyway_draw_keyed_bore/keyway_draw_section (server.py:2204/2226) -> keyway.draw_keyed_bore/draw_keyway_section", + "MCP titleblock_apply_iso_a3 (server.py:2248) -> apply_iso_a3_titleblock (titleblock.py:57)", + "MCP drawing_finalize (server.py:2281) -> DrawingValidator().run (validator.py:63)", + "MCP drawing_critique (server.py:2366) -> backend.drawing_critique -> ezdxf_backend.py:1771 -> critique.run_critique (critique.py:229); COM backend raises NotImplementedError (com_backend.py:1641)", + "MCP drawing_plan (server.py:2335) -> backend.drawing_plan -> ezdxf_backend.py:1753 builds PlanSpec and stores self._plan_spec; COM raises NotImplementedError (com_backend.py:1637)", + "MCP drawing_apply_iso_layers (server.py:2437) -> ezdxf_backend.py:1921 -> layers.apply_layer_set; COM raises NotImplementedError (com_backend.py:1653)", + "drawing_new bootstrap (server.py:284) -> ensure_engineering_layers -> apply_layer_set('mech')" + ], + "dependencies": [ + "math (gear, critique, section)", + "ezdxf (indirectly via AutoCADBackend; engineering imports backends.base only under TYPE_CHECKING)", + "backends.base.AutoCADBackend (all draw_* / check_* call its async entity_create_*/entity_list/layer_list/linetype_list/drawing_info/view_screenshot methods)", + "server.py registers all wired functions as @mcp.tool", + "engineering.critique imports engineering.layers (ENGINEERING_LAYERS) and engineering.plan_spec", + "engineering.gear imports engineering.keyway", + "re, dataclasses, pathlib (validator); dataclasses (titleblock, plan_spec)" + ], + "risks": [ + { + "title": "Engineering layer lineweights truncated to 0 by int() in layer_create", + "severity": "high", + "location": "engineering/layers.py:124 -> backends/ezdxf_backend.py:1034", + "detail": "apply_layer_set passes float mm lineweights from ENGINEERING_LAYERS (0.50, 0.25, 0.18, 0.13, 0.05) into backend.layer_create(lineweight=...), which does int(lineweight) (ezdxf_backend.py:1034). All sub-1.0 values floor to 0, so EVERY engineering layer is written with lineweight 0 in the DXF instead of its ISO value. ezdxf expects integer hundredths-of-mm (50=0.50mm) or AutoCAD enum values, not raw mm. Net effect: the entire ISO-128 lineweight discipline that CLAUDE.md advertises is silently lost in output, and drawing_apply_iso_layers/drawing_new bootstrap produce flat 0-weight layers." + }, + { + "title": "drawing_critique iso128 check is effectively dead because lineweights are 0 and units are mis-asserted", + "severity": "high", + "location": "engineering/critique.py:46-63", + "detail": "_check_iso128 reads LayerInfo.lineweight (set to float(raw_dxf_lineweight) at ezdxf_backend.py:189 — NOT normalised to mm, contradicting the comment at critique.py:53 'the layer dataclass already normalises to mm'). Combined with the int()-truncation bug above, every layer reports lw<=0 and is skipped at critique.py:50, so the ISO-128 lineweight gate can never fire. If lineweights were ever stored correctly as hundredths (e.g. 25), the check compares 25 against the mm set {0.13..2.0} and only the lw/100.0 fallback saves it — the unit handling is unverified/fragile." + }, + { + "title": "dim_overlap critique never finds dimensions on the ezdxf backend (property names mismatch)", + "severity": "high", + "location": "engineering/critique.py:194-197 vs backends/ezdxf_backend.py:166-168", + "detail": "_check_dim_overlap reads e.properties['text_position'] / ['defpoint'] / ['insertion_point']. The ezdxf backend's property extractor for DIM* types (ezdxf_backend.py:166-168) only sets props['dim_type'] and none of those three keys, so dim_points stays empty and the check is a silent no-op. Additionally ezdxf's add_linear_dim produces a DIMENSION entity (group/anonymous block); the extractor branch keys on DIMLINEAR/DIMALIGNED/etc., which may not match the reported dxftype, so even dim_type may be absent. The overlap check cannot fire as wired." + }, + { + "title": "Involute flank prepends a radial jump from root to base circle (no trochoid/fillet)", + "severity": "medium", + "location": "engineering/gear.py:40-50", + "detail": "generate_involute_flank appends (root_r, 0.0) then jumps to the involute starting at (base_r, 0.0). Between root_r and base_r the true tooth flank is a trochoidal fillet, not a straight radial segment at angle 0. The produced polyline draws a straight spoke from root to base then the involute — geometrically the dedendum region is wrong and the corner at base radius is a kink. For root_r= 0 and base-flank root point lies on root circle", + "severity": "medium", + "location": "engineering/gear.py:163-190 and 40-45", + "detail": "generate_full_gear_outline builds the inter-tooth root arc using a_root_left/a_root_right taken from base_right[0]. When root_r < base_r, base_right[0] is (root_r,0) rotated by rot, so it lies on root_r — OK. But when root_r >= base_r (high tooth counts / large PA), generate_involute_flank returns points starting at (base_r,0) (root point NOT prepended, gear.py:40 guard), so base_right[0] is on base_r not root_r, yet the root arc is drawn at radius root_r (gear.py:189) — the arc endpoints will not coincide with the flank start, leaving a gap/overlap in the closed outline. Also root_r = pitch_r - 1.25*module can be <=0 for teeth<=2, producing a degenerate/negative-radius arc with no guard." + }, + { + "title": "Tooth tip arc sampled with only 5 points and endpoint-stripping can drop the tip on coarse gears", + "severity": "low", + "location": "engineering/gear.py:117,161,178 and _arc_points 60-71", + "detail": "tip_arc uses n=5 then [1:-1] is taken (3 interior points). For a thin tooth the tip arc spans a tiny angle and 3 samples is coarse; more importantly generate_tooth_profile (gear.py:117-121) uses tip_arc[1:-1] but includes the full right_flank and reversed left_flank whose last points ARE the tip endpoints, so the tip is represented — but generate_tooth_profile is itself unwired (see smells). In generate_full_gear_outline the same [1:-1] slice is fine. Low risk, but tip fidelity is fixed at 3 interior points regardless of module." + }, + { + "title": "untrimmed_corner critique is O(n^2) over all line endpoints up to 5000 lines", + "severity": "low", + "location": "engineering/critique.py:127-151", + "detail": "Builds up to 2*5000=10000 endpoints and does a nested loop (~5e7 distance comparisons worst case) every critique run. A full gear outline is a single polyline so it is exempt, but title blocks + sections generate many LINE entities; on a busy drawing this check is slow and runs on the single COM STA thread or via to_thread. Functional but a latency risk at the 5000-line limit." + }, + { + "title": "Validator title check substring 'HELICAL SPUR' / 'SPUR' is brittle and false-positive prone", + "severity": "low", + "location": "engineering/validator.py:166-181", + "detail": "Joins all TEXT/MTEXT on the TEXT layer and substring-matches 'HELICAL SPUR' and 'SPUR'. Any annotation legitimately containing 'SPUR' (e.g. a note) on a helical part triggers an error finding that blocks drawing_finalize. Also only fires when expected['helix_angle'] is truthy, so a helical gear finalized without expected metadata is never checked. Heuristic, not load-bearing, but can wrongly fail finalize." + }, + { + "title": "DIN 6885 lookup boundary condition has redundant/confusing guard", + "severity": "low", + "location": "engineering/keyway.py:47-48", + "detail": "Loop condition `b_min < bore <= b_max or (bore == b_min and b_min == TABLE[0][0])`. The second clause only special-cases bore==6 (the very first b_min) because the half-open intervals use b_min < bore. Any other exact b_min boundary (e.g. bore==8, ==10, ==12...) falls into the PREVIOUS row's b_max (8 matched by row (6,8,...) since 6<8<=8), which is the intended DIN behaviour, but the special clause for only the first row is asymmetric and easy to misread; a bore exactly equal to a non-first b_min silently maps to the lower row. Worth a comment/test." + } + ], + "smells": [ + { + "title": "section.py (cutting_plane_line, apply_section_hatch) is entirely unwired/dead", + "severity": "medium", + "location": "engineering/section.py:12,65", + "detail": "Both functions are exported via __init__.py:35,65-66 but grep of server.py and backends finds zero references outside the engineering package and no tests. No MCP tool exposes cutting_plane_line or apply_section_hatch. draw_gear_section_aa builds its own hatch inline (gear.py:385) rather than calling apply_section_hatch, and section A-A draws no cutting-plane line at all. The whole module is reachable only via direct import that nothing performs — dead code." + }, + { + "title": "generate_tooth_profile is unwired and untested", + "severity": "low", + "location": "engineering/gear.py:74-123", + "detail": "Exported (__init__.py:9,59) but no caller: drawing uses generate_full_gear_outline instead, and test_engineering_gear.py imports involute_xy/generate_involute_flank/generate_full_gear_outline but NOT generate_tooth_profile. It duplicates ~80% of generate_full_gear_outline's single-tooth math (gear.py:81-117 vs 137-161). Dead/duplicated single-tooth path." + }, + { + "title": "Duplicated involute/rot/radii math between generate_tooth_profile and generate_full_gear_outline", + "severity": "low", + "location": "engineering/gear.py:81-117 and 137-161", + "detail": "pitch_r/base_r/outer_r/root_r, theta_pitch, inv_pitch, rot, base_right, base_left, tip-arc-angle blocks are copy-pasted between the two functions. Any fix to the flank math (e.g. the root-trochoid bug) must be applied twice; they have already diverged (generate_tooth_profile computes unused t_pitch at gear.py:94, the outline does not)." + }, + { + "title": "COM backend stubs out drawing_plan/critique/apply_iso_layers/construction_* — premium workflow is ezdxf-only", + "severity": "medium", + "location": "backends/com_backend.py:1637-1654", + "detail": "drawing_plan (Task #8), drawing_critique (Task #8), construction_xline/clear (Task #7), drawing_apply_iso_layers (Task #9) all raise NotImplementedError on the COM backend. CLAUDE.md presents the premium plan->apply_iso_layers->critique->finalize workflow as the production path, but on live AutoCAD (the COM backend) half of it is unimplemented. drawing_finalize/DrawingValidator and the gear/keyway/titleblock draw tools DO work on COM (they only use generic entity_create_* / entity_list), but the planning+critique gate does not. Doc-vs-code drift / parity gap." + }, + { + "title": "iso128 critique comment claims layer lineweight is normalised to mm; it is not", + "severity": "medium", + "location": "engineering/critique.py:53-54 vs backends/ezdxf_backend.py:189", + "detail": "Comment: 'the layer dataclass already normalises to mm'. _layer_info_dxf stores float(lw) of the raw DXF lineweight with no /100 normalisation (ezdxf_backend.py:184-189). The comment is false and masks the lineweight-unit confusion driving the iso128 dead-check risk above." + }, + { + "title": "draw_keyed_bore intentionally creates an untrimmed-looking notch that drawing_critique would not flag but isn't trimmed into the bore", + "severity": "low", + "location": "engineering/keyway.py:88-94", + "detail": "The keyway is an open 4-point polyline laid over the full bore circle; the bore circle is NOT trimmed at the keyway mouth, so the circle arc still crosses the keyway opening. This is a common schematic convention but contradicts CLAUDE.md rule 5 ('corners must be explicit', use entity_trim). The untrimmed_corner critique only inspects LINE-LINE endpoints (critique.py:116) so it cannot see the circle/polyline overlap — the inconsistency goes unflagged." + }, + { + "title": "TitleBlockMetadata.company defaults to a hardcoded vendor name 'Anka-Makine'", + "severity": "low", + "location": "engineering/titleblock.py:54", + "detail": "company: str = 'Anka-Makine' bakes a specific company into the generic title-block default. Any drawing finalized without overriding company silently stamps this name. Should default to empty string or be configuration-driven." + }, + { + "title": "Title text horizontal centering is a crude char-count heuristic", + "severity": "low", + "location": "engineering/titleblock.py:171", + "detail": "title_x = center - len(title)*TITLE_HEIGHT*0.3 estimates string width from character count, which is wrong for proportional fonts and will mis-center long/short titles. Cosmetic but undermines the 'production-quality deterministic' claim." + }, + { + "title": "__init__.py guards titleblock/validator imports behind try/except ImportError 'while Agent B is in flight'", + "severity": "low", + "location": "engineering/__init__.py:38-51", + "detail": "TitleBlockMetadata/apply_iso_a3_titleblock/DrawingValidator/etc. are set to None on ImportError, but both modules now exist and are imported unconditionally by server.py (server.py:2266,2294). If those modules raise a real ImportError (e.g. a typo), the package silently degrades the symbols to None and server.py's 'from engineering import ... TitleBlockMetadata' would import None instead of failing loudly. Stale scaffolding comment + error-swallowing." + }, + { + "title": "PID_LAYERS / ISO13567_LAYERS reference linetypes DASHED/DASHDOT loaded by ensure_standard_linetypes, but mech path also loads them unnecessarily", + "severity": "low", + "location": "engineering/layers.py:71,110", + "detail": "STANDARD_LINETYPES includes DASHED and DASHDOT which only the pid/iso13567 sets use, yet apply_layer_set always calls ensure_standard_linetypes (layers.py:110) for every set including 'mech'. Minor: loads 2 unused linetypes for mechanical drawings. Harmless but indicates the linetype set isn't keyed to the chosen layer set." + } + ], + "notes": "Wiring verdict (grep of server.py + backends): WIRED to MCP tools = draw_helical_gear_front_view, draw_spur_gear_front_view, draw_gear_section_aa, draw_keyed_bore, draw_keyway_section, apply_iso_a3_titleblock+TitleBlockMetadata, DrawingValidator, PlanSpec (via ezdxf drawing_plan), run_critique (via ezdxf drawing_critique), apply_layer_set/ensure_engineering_layers (via drawing_apply_iso_layers + drawing_new bootstrap). Pure-math involute_xy/generate_involute_flank/generate_full_gear_outline are used transitively by the gear draw tools and covered by tests. UNWIRED/DEAD = section.py cutting_plane_line & apply_section_hatch (no references outside engineering/, no tool, no test) and gear.py generate_tooth_profile (exported but no caller, no test, duplicates outline math). PARITY GAP = drawing_plan/drawing_critique/drawing_apply_iso_layers/construction_* raise NotImplementedError on the COM backend (com_backend.py:1637-1654), so the entire 'premium workflow' planning+critique gate only functions on ezdxf, despite CLAUDE.md presenting it as the production path. Highest-impact bug to verify first: int()-truncation of mm lineweights in layer_create (ezdxf_backend.py:1034) which nullifies the ISO-128 lineweight system AND makes the iso128 critique a permanent no-op. Did not run the test suite; findings are from static reading of the cited files." + }, + { + "module": "tests/ (test suite) — coverage map for AutoCAD MCP Pro", + "purpose": "Audit of the pytest suite: a single shared ezdxf-backed `backend` fixture (tests/conftest.py) drives 14 test files. The suite exercises the ezdxf backend, the security sanitizers (in isolation), the engineering package (gear/keyway/titleblock/validator), corner ops (trim/extend/fillet/chamfer), and the premium meta-tools (plan/critique/snap/construction/iso-layers/dimension_auto/select_smart). The COM backend, the server-layer MCP tool wiring, and several backend methods are entirely untested.", + "public_surface": [ + "conftest.py::backend fixture — EzdxfBackend + connect + drawing_new (the ONLY fixture; no COM fixture exists)", + "test_ezdxf_backend.py (16): CRUD, type-filter exact match, rotate/scale/mirror/array transforms, transaction commit+rollback, block_create_from_entities", + "test_entity_creation.py (13): all entity_create_* types (circle/arc/polyline/text/mtext/hatch/spline/ellipse/point + layer/color)", + "test_analysis.py (12): entity_copy/offset/array_rect/set_properties/delete_many + analysis_stats/region/distance/area/bbox/select_by_layer/select_by_type", + "test_layer_block.py (12): layer_create/list/delete/set_current/modify/freeze-thaw/lock-unlock/hide-show + block create/insert/explode/get_attributes", + "test_meta_tools.py (32): drawing_plan, drawing_critique (3 of 6 focuses), point_from_snap (all snaps), construction_*, drawing_apply_iso_layers (mech/pid/iso13567), dimension_auto (chain/baseline only), entity_select_smart, plan->draw->critique pipeline", + "test_security.py (29): validate_path, sanitize_command, sanitize_lisp — all in isolation, never via server tools", + "test_entity_corner_ops.py (18): trim/extend/fillet/chamfer ezdxf-only (header explicitly skips COM)", + "test_engineering_validator.py (9): DrawingValidator 8-step checks", + "test_engineering_gear.py (9), test_engineering_keyway.py (5), test_engineering_titleblock.py (7)", + "test_batch_template.py (9), test_drawing.py (8), test_dimensions.py (5, 2 xfail)" + ], + "key_flows": [ + "TEST COUNT: 184 test functions actually collected (pytest --collect-only reports '184 tests collected'), across 14 test files — NOT 13. README.md:83 claims '104 tests'; the real number is 184 (README understates by 80).", + "All async tests share ONE function-scoped ezdxf fixture (tests/conftest.py:8-15); every test re-creates a fresh in-memory DXF doc. No COM fixture exists anywhere.", + "Security is tested only at the pure-function level (security.py). The server.py MCP tool wiring (server.py:2035 sanitize_command, :2052 sanitize_lisp, :276/:306/:324/:340/:354/:368/:2298/:2302 validate_path) has NO integration test proving the tools actually invoke the sanitizers.", + "COM backend (backends/com_backend.py, ~80 async methods) has ZERO coverage — no test imports ComBackend/win32com; test_entity_corner_ops.py header states COM tests are skipped.", + "Engineering section module (engineering/section.py: cutting_plane_line, apply_section_hatch) has ZERO coverage." + ], + "dependencies": [ + "pytest>=8.0", + "pytest-asyncio>=0.23 (asyncio_mode='strict' in pyproject.toml:53-54)", + "pytest-cov>=5.0 (declared but NO coverage gate / addopts configured)", + "ezdxf (only backend the suite can exercise)", + "fastmcp.exceptions.ToolError (security tests)" + ], + "risks": [ + { + "title": "COM backend has zero automated coverage", + "severity": "high", + "location": "backends/com_backend.py:374-1659 (entire file); tests/ has no ComBackend import", + "detail": "~80 async COM methods including transaction_begin/commit/rollback (com_backend.py:1396-1422 use StartUndoMark/EndUndoMark/SendCommand '_UNDO B'), entity_trim/extend/fillet/chamfer via SendCommand string-building (1549+), and the CMDACTIVE deadlock-guard logic (_safe_send_command, 1497-1547) are never tested. This is the production path on Windows. The undo-mark transaction model is semantically different from the ezdxf snapshot model, so the passing ezdxf transaction tests (test_ezdxf_backend.py:149-168) give false confidence that transactions work on COM." + }, + { + "title": "Server-layer security wiring is unverified (sanitizer-bypass blind spot)", + "severity": "high", + "location": "server.py:2035, 2052, 2298-2302; tests/test_security.py (whole file)", + "detail": "test_security.py tests sanitize_command/sanitize_lisp/validate_path as standalone functions but never asserts that the MCP tools system_run_command/system_run_lisp/drawing_* actually call them. If a future refactor drops the sanitize_command(command) call at server.py:2035, every security test still passes while raw commands reach AutoCAD's SendCommand (com_backend.py:1483). The highest-value security guarantee (tool -> sanitizer -> backend) is the one thing not covered." + }, + { + "title": "LISP/command sanitizer bypass edge-cases untested", + "severity": "high", + "location": "security.py:138-179 (_DANGEROUS_LISP_PATTERN) and tests/test_security.py:177-231", + "detail": "The code comment at security.py:138-140 explicitly claims to defend against aliasing tricks like '(setq f command)(f \"ERASE\")', but NO test exercises that bypass. Also untested: command sanitizer word-boundary evasion (e.g. dot-prefixed '.ERASE', leading-underscore variants beyond the few listed, concatenated/whitespace-padded payloads), UNC paths (\\\\server\\share) and '~' home expansion in validate_path, and symlink escape from ALLOWED_PATHS. The regex-based denylist is exactly the kind of thing that needs adversarial test cases and has none." + }, + { + "title": "Critique focuses iso128 / layer_color / dim_overlap failure paths untested", + "severity": "medium", + "location": "engineering/critique.py:37-65 (_check_iso128), :67-89 (_check_layer_color), :183-218 (_check_dim_overlap); tests/test_meta_tools.py:39-71", + "detail": "drawing_critique exposes 6 focuses (critique.py:220-225). Tests only assert the positive-failure path for construction_left, untrimmed_corner, duplicate_entities (test_meta_tools.py). iso128, layer_color, and dim_overlap are only exercised via the clean-drawing case (test_critique_clean_drawing, :40-44) which asserts they DON'T fire — their actual detection logic is never proven to catch a violation. The unknown-focus error branch (critique.py:241-242) is also untested." + }, + { + "title": "dimension_auto 'ordinate' style untested", + "severity": "medium", + "location": "backends/ezdxf_backend.py:1988-1989; tests/test_meta_tools.py:173-196", + "detail": "dimension_auto supports chain/baseline/ordinate (ezdxf_backend.py:1929). Tests cover chain (test_chain_creates_one_dim_per_segment) and baseline, plus the unknown-style raise and empty-handles cases, but the ordinate branch (1988+, the most complex — marks X then Y of each endpoint) has no test." + }, + { + "title": "Aligned/angular dimensions permanently xfail — feature effectively unverified", + "severity": "medium", + "location": "tests/test_dimensions.py:16-27", + "detail": "dimension_aligned and dimension_angular are marked xfail(strict=False) with 'ezdxf API parameter mismatch'. These are advertised MCP tools (server section 3, 5 dimension_* tools). They are shipped broken in the ezdxf backend with the test acknowledging the breakage rather than fixing it; strict=False means even an accidental fix would not flip to a failure to alert anyone." + }, + { + "title": "Multiple backend methods have no test at any level", + "severity": "medium", + "location": "backends/ezdxf_backend.py / com_backend.py: system_get_variable(1451), system_set_variable(1457), system_status(1426), drawing_close(552), drawing_export_pdf(528), entity_create_block_ref(722), block_set_attributes(1239), linetype_list(1112), linetype_load(1118), view_zoom_extents/window(1371/1379), view_screenshot(1386)", + "detail": "Grep of tests/ for these method names returns zero hits. linetype_load is load-bearing for the engineering linetype bootstrap; view_screenshot is the matplotlib render path for ezdxf; drawing_export_pdf is an advertised tool. All are uncovered on both backends." + }, + { + "title": "Transaction nesting / error semantics untested", + "severity": "low", + "location": "tests/test_ezdxf_backend.py:149-168", + "detail": "Only single begin->create->rollback and begin->create->commit are tested. Nested transactions, rollback-without-begin, commit-without-begin, and rollback restoring layers/blocks (not just entities) are untested. The ezdxf snapshot stack (_undo_stack per CLAUDE.md) could mishandle nesting and the suite would not detect it." + } + ], + "smells": [ + { + "title": "README test count is stale/wrong (104 vs actual 184)", + "severity": "medium", + "location": "README.md:83", + "detail": "README claims '104 tests across drawing, entity, dimension, layer, block, analysis, batch/template, security suites'. pytest --collect-only collects 184. The README list also omits the engineering, corner-ops, and meta-tools suites entirely (which together add ~80 tests). Doc-vs-code drift." + }, + { + "title": "Security tests mutate global config.settings with fragile manual restore", + "severity": "medium", + "location": "tests/test_security.py:67-107, 156-169, 218-231", + "detail": "test_allowed_paths_enforcement and the *_bypass tests reassign module-global config.settings (config.settings = Settings()) and rely on manual teardown lines to restore it. test_allowed_paths_enforcement (84-88) even does config.settings = Settings.__new__(Settings) then sets attributes by hand. If any assertion mid-test raises, the global is left polluted and can cross-contaminate other tests depending on collection order. Should use a fixture with guaranteed teardown / monkeypatch.setattr on config.settings." + }, + { + "title": "Defensive 'if start:' / 'if start and end:' guards hide assertion skips", + "severity": "medium", + "location": "tests/test_batch_template.py:30-34, 116-128; tests/test_analysis.py:70-72", + "detail": "test_batch_modify_move asserts coordinates only 'if start:' — if properties.get('start') is empty the test passes without checking anything (vacuous pass). Same in test_validation_zero_length_detection ('if start and end:'). test_analysis_entity_stats has a deeply nested .get() fallback chain that makes the assertion 'total >= 3' nearly unfalsifiable. These tests can stay green while the underlying property serialization breaks." + }, + { + "title": "Many 'template/validation' tests assert nothing about the feature they name", + "severity": "medium", + "location": "tests/test_batch_template.py:51-113", + "detail": "test_template_architectural_layers / _mechanical_ / _electrical_ just call layer_create in a loop and assert the layers exist — there is no 'template' code under test (templates are inlined in the test). test_validation_* reimplement empty-layer/zero-length detection in the test body instead of calling a backend validator. These are testing ezdxf layer_create N times over, not any template or validation feature; low value, misleading names." + }, + { + "title": "Validator clean-drawing test needs a synthetic HIDDEN linetype workaround", + "severity": "low", + "location": "tests/test_engineering_validator.py:17-25, 28-39", + "detail": "_ensure_hidden_linetype reaches into backend._require_doc() and manually adds a HIDDEN linetype pattern because 'ezdxf doesn't ship a HIDDEN pattern'. The test bypasses the public API and patches internal state to make the validator pass, indicating the validator's linetype check and the ezdxf bootstrap (ensure_standard_linetypes) disagree about HIDDEN — the workaround masks that mismatch rather than fixing it." + }, + { + "title": "Validator title-check test accepts either of two finding codes", + "severity": "low", + "location": "tests/test_engineering_validator.py:81", + "detail": "assert 'title_helical_spur' in codes or 'title_says_spur_for_helical' in codes — the test does not pin which code the validator emits, so a refactor could silently change the contract while the test stays green. Indicates code/test ambiguity about the finding's canonical code." + }, + { + "title": "No coverage gate despite pytest-cov dependency", + "severity": "low", + "location": "pyproject.toml:53-54 (only asyncio_mode set); pytest-cov declared at dev deps", + "detail": "pytest-cov>=5.0 is a declared dev dependency but [tool.pytest.ini_options] has no addopts/--cov, no fail-under threshold, and no testpaths. Coverage is never measured in CI (the .github/workflows/ci.yml is also deleted per git status), so the COM/section/critique gaps go unnoticed by tooling." + }, + { + "title": "CI workflow deleted — suite not enforced", + "severity": "medium", + "location": ".github/workflows/ci.yml (deleted, per git status 'D .github/workflows/ci.yml')", + "detail": "The only CI workflow is staged for deletion. With no CI, the 184 tests (and the 2 xfail dimension regressions) are not gated on PRs; regressions in untested COM paths or sanitizer wiring would ship undetected." + } + ], + "notes": "Verified counts by hand: pytest --collect-only prints '184 tests collected'; per-file grep of 'def test_' matches (analysis 12, batch_template 9, dimensions 5, drawing 8, gear 9, keyway 5, titleblock 7, validator 9, corner_ops 18, entity_creation 13, ezdxf_backend 16, layer_block 12, meta_tools 32, security 29 = 184). 14 test files (prompt expected ~13). Highest-value untested paths, ranked: (1) COM backend entirely — esp. transactions, run_command/run_lisp SendCommand, corner-ops string-building; (2) server tool -> sanitizer wiring (no integration test); (3) LISP aliasing-bypass + path edge cases the code comments claim to defend; (4) critique iso128/layer_color/dim_overlap detection; (5) dimension_auto ordinate + aligned/angular (xfail). The ezdxf backend itself is reasonably well covered for happy paths." + }, + { + "module": "Documentation-vs-reality drift audit (CLAUDE.md Premium Drawing Rules + README.md claims)", + "purpose": "Verify that the 12 premium tools referenced in CLAUDE.md's \"Premium Drawing Rules\" actually exist in server.py + both backends, and that README.md's concrete claims (87 tools, 104 tests, Dockerfile, .env.example, benchmarks/, LICENSE, .github/workflows/ci.yml) match the repo on disk.", + "public_surface": [ + "server.py: 109 @mcp.tool decorators registered (CLAUDE.md/README claim '87 tools' but explicitly defer to system_about for the live count)", + "backends/base.py: AutoCADBackend ABC declares all 12 premium-rule methods (entity_trim:307, entity_fillet:325, entity_chamfer:335, drawing_plan:513, drawing_critique:527, point_from_snap:535, construction_xline:546, construction_clear:555, drawing_apply_iso_layers:563, dimension_auto:571, entity_select_smart:581)", + "backends/ezdxf_backend.py: all 12 premium-rule methods implemented with real bodies", + "backends/com_backend.py: entity_trim/fillet/chamfer implemented via SendCommand; the other 8 premium meta-tools raise NotImplementedError" + ], + "key_flows": [ + "Premium-rule tool resolution: server.py @mcp.tool wrapper -> _backend(ctx).(...) -> ezdxf_backend (full impl) OR com_backend (8 of 12 raise NotImplementedError)", + "Test coverage path: tests/conftest.py backend fixture instantiates EzdxfBackend ONLY -> tests/test_meta_tools.py exercises all 12 premium tools against ezdxf, never against COM -> COM stubs are untested and silently unverified", + "README file-existence claims vs repo: LICENSE and benchmarks/ absent; .github/workflows/ci.yml tracked-but-deleted; Dockerfile + .env.example present" + ], + "dependencies": [ + "server.py", + "backends/base.py", + "backends/ezdxf_backend.py", + "backends/com_backend.py", + "engineering/plan_spec.py", + "engineering/critique.py", + "engineering/layers.py", + "tests/conftest.py", + "tests/test_meta_tools.py", + "README.md", + "CLAUDE.md" + ], + "risks": [ + { + "title": "8 premium meta-tools raise NotImplementedError on COM backend while docs present them as production-ready", + "severity": "high", + "location": "backends/com_backend.py:1637-1660", + "detail": "drawing_plan (1637-1639), drawing_critique (1641-1642), point_from_snap (1644-1645), construction_xline (1647-1648), construction_clear (1650-1651), drawing_apply_iso_layers (1653-1654), dimension_auto (1656-1657), entity_select_smart (1659-1660) all `raise NotImplementedError(\"... pending Task #N\")`. CLAUDE.md 'Premium Drawing Rules' calls these 'non-negotiable for production engineering output' and the 'Standard Premium Workflow' template chains drawing_plan->drawing_apply_iso_layers->construction_xline->dimension_auto->drawing_critique->construction_clear. On a live AutoCAD (COM) session — README's primary 'real-time control' use case, listed 'Verified' for Claude Desktop/Cursor — every one of these steps throws. The documented premium workflow is fully functional ONLY on the headless ezdxf backend.", + "severity_note": "high" + }, + { + "title": "COM stubs for premium tools are never tested; test fixture is ezdxf-only", + "severity": "medium", + "location": "tests/conftest.py:8-15", + "detail": "The shared `backend` fixture instantiates `EzdxfBackend()` exclusively (import at line 5, construction at line 11). tests/test_meta_tools.py (32 test functions) and all other suites run only against ezdxf, so the COM NotImplementedError stubs at com_backend.py:1637-1660 have zero coverage. README line 83 claims '104 tests across ... security suites' and line 102-103 mark COM hosts 'Verified', but no COM code path — including the premium stubs — is exercised by the suite.", + "severity_note": "medium" + } + ], + "smells": [ + { + "title": "README claims a LICENSE file that does not exist", + "severity": "high", + "location": "README.md:409", + "detail": "Line 409 'MIT — see [LICENSE](LICENSE).' and line 6 badge 'MIT' link/reference a LICENSE file. `git ls-files LICENSE` returns nothing and `ls LICENSE*` => 'No such file or directory'. The repo has no LICENSE file despite asserting MIT licensing with a copyright line (411-412). This is a legal/licensing drift, not just cosmetic." + }, + { + "title": "README references a benchmarks/ directory that does not exist", + "severity": "low", + "location": "README.md:302", + "detail": "Line 302 'Numbers will land in [`benchmarks/`](benchmarks/)' (also linked in TOC line 39 and Roadmap line 367). `ls benchmarks` => 'No such file or directory' and `git ls-files benchmarks/` is empty. The surrounding prose marks it 'in active development / coming', so this is forward-looking, but the markdown link is dead today." + }, + { + "title": "README documents .github/workflows/ci.yml but it is deleted on disk", + "severity": "high", + "location": "README.md:358", + "detail": "README 'Project structure' line 358 lists '.github/workflows/ci.yml' and line 84 advertises 'GitHub Actions CI'. The file is git-tracked but staged-deleted: git status shows 'D .github/workflows/ci.yml' and `ls .github/workflows` => 'No such file or directory'. CI is therefore broken/removed while docs still claim it exists. Note the deletion is unstaged-vs-index inconsistent (appears as a working-tree deletion), so a commit would drop CI entirely." + }, + { + "title": "README test-count claim (104) does not match the suite (184 test functions)", + "severity": "medium", + "location": "README.md:83", + "detail": "Line 83 states '104 tests'. `grep -rcE 'def test_' tests/*.py` totals 184 test functions across 14 files (analysis 12, batch_template 9, dimensions 5, drawing 8, engineering_gear 9, engineering_keyway 5, engineering_titleblock 7, engineering_validator 9, entity_corner_ops 18, entity_creation 13, ezdxf_backend 16, layer_block 12, meta_tools 32, security 29). The number is stale by ~80 tests — the engineering/meta-tool suites were added after the README count was written." + }, + { + "title": "README 'Project structure' tests/ listing is stale — omits 6 of 14 test files", + "severity": "low", + "location": "README.md:347-355", + "detail": "The documented tree lists only test_drawing, test_entity_creation, test_dimensions, test_layer_block, test_analysis, test_batch_template, test_ezdxf_backend, test_security. It omits the entire engineering/meta layer: test_engineering_gear.py, test_engineering_keyway.py, test_engineering_titleblock.py, test_engineering_validator.py, test_entity_corner_ops.py, test_meta_tools.py. The same tree (lines 338-359) also omits the engineering/ package and the premium tooling entirely." + }, + { + "title": "README/Author narrative claims a battle-tested public benchmark and verified COM workflow that the code contradicts", + "severity": "medium", + "location": "README.md:16-20", + "detail": "Lines 16-20 ('watching it shave hours off my week without a single misstep on critical drawings', 'a public benchmark suite is on the way') and Highlights line 84 present the premium workflow as production-proven on live AutoCAD. Combined with the COM NotImplementedError stubs (com_backend.py:1637-1660), the documented production premium pipeline cannot have run end-to-end on COM. Doc-vs-code overclaim." + }, + { + "title": "Tool-count drift: 109 @mcp.tool decorators vs documented '87 tools'", + "severity": "low", + "location": "README.md:6", + "detail": "`grep -cE '@mcp\\.tool' server.py` = 109; README line 6/18/66/98/120 and CLAUDE.md repeatedly say '87 tools'. Both docs DO add a dynamic-count caveat (README line 248 'the runtime number is authoritative', CLAUDE.md 'never hardcode it'), so this is a soft/documented drift rather than a hard lie, but the headline numbers are stale by ~22 tools." + }, + { + "title": "Premium skill + cursor rule directories the docs depend on are git-deleted", + "severity": "medium", + "location": "CLAUDE.md (Premium Drawing Rules section)", + "detail": "CLAUDE.md states 'The full discipline + workflow templates live in .claude/skills/autocad-mcp-premium/'. git status shows 28 staged deletions covering the entire .claude/skills/autocad-mcp-premium/ tree (SKILL.md + examples/ + references/) and the entire .cursor/ tree (.cursor/rules/Master-Rules.mdc, .cursor/skills/autocad-mcp/ SKILL.md, assets, examples, references, scripts). The referenced skill documentation has been removed, so the CLAUDE.md pointer is dangling and the Cursor integration advertised in README line 103 ('Verified') lost its bundled rules/skill." + } + ], + "notes": "Per-tool implementation matrix for the 12 CLAUDE.md Premium Drawing Rules tools (server = @mcp.tool registered in server.py; ezdxf = real impl in ezdxf_backend.py; com = state in com_backend.py): drawing_plan — server:2335, ezdxf:1753 (real, holds PlanSpec), COM:1637 NotImplementedError. point_from_snap — server:2384, ezdxf:1775 (real), COM:1644 NotImplementedError. construction_xline — server:2403, ezdxf:1890 (real), COM:1647 NotImplementedError. construction_clear — server:2422, ezdxf:1910 (real), COM:1650 NotImplementedError. drawing_apply_iso_layers — server:2437, ezdxf:1921 (real, delegates engineering.layers), COM:1653 NotImplementedError. entity_trim — server:877, ezdxf:1535 (real), COM:1549 (real, SendCommand _TRIM). entity_fillet — server:917, ezdxf:1658 (real), COM:1592 (real, SendCommand _FILLET). entity_chamfer — server:934, ezdxf:1719 (real), COM:1613 (real, SendCommand _CHAMFER). entity_select_smart — server:2473, ezdxf:2008 (real), COM:1659 NotImplementedError. dimension_auto — server:2453, ezdxf:1926 (real), COM:1656 NotImplementedError. drawing_critique — server:2366, ezdxf:1771 (real, delegates engineering.critique), COM:1641 NotImplementedError. drawing_finalize — server:2281 (real; calls validator); not a backend method per se (orchestrated in server). NONE of the 12 are fully NONEXISTENT/aspirational — all are registered in server and all have real ezdxf impls. The drift is COM-side: 8 of 12 (drawing_plan, drawing_critique, point_from_snap, construction_xline, construction_clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart) are NotImplementedError stubs on the COM backend.\\n\\nREADME file-existence verdicts: '87 tools' = STALE (actual 109 @mcp.tool, but caveated). '104 tests' = STALE (184 test functions). Dockerfile = EXISTS (git-tracked + on disk). .env.example = EXISTS (git-tracked + on disk). benchmarks/ = ABSENT. LICENSE = ABSENT (despite MIT claim + dead link). .github/workflows/ci.yml = TRACKED BUT DELETED on disk (git status 'D')." + } + ], + "research": [ + { + "topic": "Recent academic work (2025-2026) on LLM/text-to-CAD, CAD program generation, LLM geometric reasoning, and parametric CAD synthesis — and how it applies to AutoCAD MCP Pro (a FastMCP server that drives AutoCAD via tool calls)", + "summary": "The literature strongly validates this repo's core architectural bets and points to concrete upgrades. Three findings independently confirm the design: (1) GeoGramBench (ICLR 2026) shows frontier LLMs score <50% on translating program code into geometry at high abstraction — direct evidence that this MCP is right to forbid coordinate-guessing and force deterministic point_from_snap() (backends/base.py:535-543) and entity_select_smart() (base.py:581-591) instead of letting the LLM compute snap points from memory (CLAUDE.md Premium Rule 2). (2) The self-improving FEA-feedback agent (2026) independently arrives at this repo's exact \"deterministic controller owns validation, LLM owns design\" split, mirroring drawing_critique/drawing_finalize (engineering/critique.py, engineering/validator.py) being deterministic while the LLM plans; its headline result — typed closed-loop feedback beats more reasoning budget — argues for making drawing_critique a routed, iterative loop rather than a one-shot gate. (3) CADFusion (ICML 2025) shows CAD is multimodal and parametric-only supervision misses the rendering gap, which this repo only partially addresses via view_screenshot (validator step 7 emits info-only \"screenshot_unavailable\"). Net opportunities: add a ProCAD-style proactive clarification step in front of drawing_plan; turn the existing screenshot into an actual visual-feedback critique focus; expose a CadQuery/parametric-script tool as a higher-abstraction macro layer over the 87 low-level tools; and adopt B-Rep/primitive natural-language grounding (FutureCAD 2026) to harden entity_select_smart and the trim/fillet/chamfer corner ops (base.py:305-342) that currently require exact handle+keep-point coordinates from the LLM.", + "findings": [ + { + "title": "GeoGramBench: Benchmarking the Geometric Program Reasoning in Modern LLMs", + "source_url": "https://arxiv.org/abs/2505.17653", + "date": "2025-05-23 (ICLR 2026)", + "takeaway": "Formalizes the Program-to-Geometry task (translate drawing code into geometric reasoning). Across 17 frontier LLMs, even the best score <50% accuracy at the highest abstraction level — LLMs are fundamentally unreliable at computing geometry from procedural descriptions.", + "relevance": "Empirical justification for this MCP's no-coordinate-guessing rule. It validates routing all snap/intersection/perpendicular computation through deterministic point_from_snap() (backends/base.py:535-543) and entity_select_smart() (base.py:581-591) rather than the LLM. Argues for expanding the deterministic-tool surface (e.g. a point_intersection/point_offset helper) so the LLM never derives coordinates, and for keeping critique checks (engineering/critique.py) symbolic rather than LLM-judged." + }, + { + "title": "Self-Improving CAD Generation Agents with Finite Element Analysis as Feedback", + "source_url": "https://arxiv.org/html/2605.17448v2", + "date": "2026-05", + "takeaway": "A deterministic controller owns execution/validation/feedback-routing while the LLM owns only design decisions and code repair. Typed pass/fail reports with margins are fed back in a closed loop. One feedback round adds +13.4pp mean requirement pass; frontier models (GPT-5.5, Opus-4.7) produce ZERO first-attempt strict passes — iteration with concrete evidence beats spending more tokens on a single attempt.", + "relevance": "This is essentially this repo's architecture (deterministic drawing_critique/drawing_finalize + LLM-driven drawing_plan), independently rediscovered. Two upgrades follow: (1) make drawing_critique a routed iterative loop the agent re-enters with compact typed reports (the Issue dataclass in plan_spec.py:64-81 already carries severity/handles/detail — turn the workflow from a one-shot gate into a repair loop), and (2) the 8-step validator (engineering/validator.py) should return per-finding actionable repair hints, not just descriptions, so the LLM's next attempt is grounded." + }, + { + "title": "CADFusion: Text-to-CAD Generation Through Infusing Visual Feedback in LLMs", + "source_url": "https://arxiv.org/abs/2501.19054", + "date": "2025-01-31 (ICML 2025)", + "takeaway": "CAD is multimodal: a two-stage method alternates sequential learning on parametric sequences with a visual-feedback stage that rewards sequences whose RENDER looks right (rendering is many-to-one, so parametric-only supervision misses errors). Significant qualitative and quantitative gains over parametric-only baselines.", + "relevance": "This MCP renders via view_screenshot (COM Win32 capture / ezdxf matplotlib) but only uses it as an info-only validator step (engineering/validator.py:206-218 emits 'screenshot_unavailable'). Opportunity: promote rendering to a first-class visual-feedback critique focus — feed the screenshot back to the agent (or a VLM judge) to catch errors symbolic checks miss (mislabeled views, wrong proportions, crossed geometry), complementing the existing iso128/untrimmed_corner/duplicate checks in critique.py." + }, + { + "title": "Clarify Before You Draw (ProCAD): Proactive Agents for Robust Text-to-CAD Generation", + "source_url": "https://arxiv.org/html/2602.03045v1", + "date": "2026-02", + "takeaway": "A clarifying agent (finite-horizon MDP, single round, minimize interactions) audits the prompt for under-specified or conflicting dimensions BEFORE code generation, then hands a corrected spec to the coder. Drops mean Chamfer distance from 7.80 to 0.63 and invalid ratio from 4.8% to 0.9% vs a Sonnet 4.5 baseline.", + "relevance": "Maps onto the mandatory drawing_plan(intent, scale, sheet) step (CLAUDE.md Premium Rule 1, base.py:512-524). drawing_plan currently records intent passively; add a proactive validation pass that detects missing/conflicting constraints (dimensions, material, tolerance, view_count vs intent) and returns targeted clarification questions before any entity_create_* call — converting reactive mid-draw failures into one cheap up-front clarification round." + }, + { + "title": "Towards High-Fidelity CAD Generation via LLM-Driven Program Generation and Text-Based B-Rep Primitive Grounding (FutureCAD)", + "source_url": "https://arxiv.org/abs/2603.11831", + "date": "2026-03-12 (rev 2026-04-26)", + "takeaway": "Lets the LLM specify geometric selections in natural language, which a BRepGround component resolves to the exact target B-Rep primitives. Specifically fixes the disconnect between parametric ops and the primitives they act on (fillets/chamfers need explicit primitive selection). SOTA via SFT+RL on a real-world CAD dataset.", + "relevance": "Directly addresses this repo's hardest LLM burden: trim/extend/fillet/chamfer (base.py:305-342) require exact cutter/boundary handles PLUS keep_x/keep_y coordinates, and entity_select_smart (base.py:581-591) uses a brittle dict predicate. A natural-language grounding layer ('fillet the top-left corner of the bracket', 'trim against the bore') resolved deterministically against the DXF/B-Rep would remove the coordinate burden GeoGramBench shows LLMs fail at, and reduce the untrimmed_corner issues critique.py:111-151 is built to catch." + }, + { + "title": "Text-to-CadQuery: A New Paradigm for CAD Generation with Scalable LLM Capabilities", + "source_url": "https://arxiv.org/html/2505.06507v1", + "date": "2025-05", + "takeaway": "Generate CadQuery (Python) code directly from text instead of low-level command sequences; the field has converged on CadQuery because Python aligns with LLMs' strong code-generation skills and gives a higher abstraction for parametric relationships and complex geometry.", + "relevance": "This MCP exposes 87 fine-grained tools; complex parts force long brittle tool-call chains. The engineering/ package (gear.py, keyway.py, titleblock.py) already proves the value of parametric Python macros over hand-drawn primitives (CLAUDE.md 'Production drawings' rule). Opportunity: add more parametric meta-tools (the gear/keyway pattern) for common features, and/or expose a sandboxed parametric-script tool so the LLM emits high-level intent that deterministic Python expands into entity_create_* calls — fewer round-trips, fewer geometry errors." + }, + { + "title": "CAD-Coder: An Open-Source Vision-Language Model for CAD Code Generation", + "source_url": "https://arxiv.org/html/2505.14646v1", + "date": "2025-05-20", + "takeaway": "A LLaVA-style VLM takes an image and outputs editable CadQuery code, fine-tuned on GenCAD-Code (built from Text2CAD into ~110K verified text-CadQuery-3D triplets); evaluated by compilation success rate and IoU.", + "relevance": "Suggests an image-to-drawing entry path for this MCP: a user-supplied sketch/photo of a part could seed drawing_plan and the parametric tool chain. Also models a useful eval harness — score generated drawings by automated geometric metrics (IoU/Chamfer over the rendered DXF) to regression-test the premium workflow, which the repo currently lacks." + }, + { + "title": "EvoCAD: Evolutionary CAD Code Generation with Vision Language Models", + "source_url": "https://arxiv.org/pdf/2510.11631", + "date": "2025-10", + "takeaway": "Treats CAD code generation as an evolutionary search over candidate CadQuery programs guided by VLM-evaluated renders, iterating populations toward a target shape rather than a single forward pass.", + "relevance": "An alternative to the single-pass plan->draw->critique flow: the deterministic critique/validator already provides a fitness signal. Generating a few candidate plans/geometries and selecting via critique scores (zero issues + visual fitness) could raise first-try quality for ambiguous intents, while keeping the deterministic critique as the selection oracle." + } + ], + "opportunities": [ + "Promote view_screenshot from an info-only validator step (engineering/validator.py:206-218) to a first-class 'visual' critique focus: render the drawing and feed it back to the agent (or a VLM judge) to catch errors symbolic checks miss — the CADFusion many-to-one rendering insight.", + "Add a ProCAD-style proactive clarification pass inside/in front of drawing_plan (base.py:512-524): detect under-specified or conflicting constraints (missing dims, material, tolerance, view_count vs intent) and return targeted questions BEFORE any entity_create_* call.", + "Turn drawing_critique into a routed iterative repair loop instead of a one-shot finalize gate: the Issue dataclass (plan_spec.py:64-81) already carries severity/handles/detail; add per-finding repair hints so each loop pass is actionable — backed by the FEA-agent result that typed closed-loop feedback beats more reasoning budget.", + "Expand the deterministic geometry-tool surface so the LLM never computes coordinates (GeoGramBench <50% finding): add point_intersection / point_offset / point_along helpers alongside point_from_snap (base.py:535-543), and a natural-language B-Rep/primitive grounding layer (FutureCAD) over the brittle trim/fillet/chamfer handle+keep-point API (base.py:305-342) and entity_select_smart predicate (base.py:581-591).", + "Add a higher-abstraction parametric layer: more engineering/ meta-tools following the gear/keyway/titleblock pattern, and/or a sandboxed parametric-script tool (CadQuery/Text-to-CadQuery style) that the LLM emits and deterministic Python expands into entity_create_* calls — fewer brittle round-trips for complex parts.", + "Build an automated geometric eval harness (IoU/Chamfer over rendered DXF, compilation/critique success rate) modeled on Text2CAD-Bench/CAD-Coder metrics to regression-test the premium workflow against a fixed brief set — the repo's tests/ cover units but not end-to-end drawing fidelity.", + "Consider candidate-generation + critique-as-fitness selection (EvoCAD pattern): generate a few plans/geometries for ambiguous intents and select the one with zero critique issues plus best visual fitness, reusing the existing deterministic critique/validator as the oracle." + ], + "confidence": "High. WebSearch and WebFetch were both available and used; all 8 findings are real arXiv papers fetched/verified this session with titles, URLs, and dates. Codebase claims are cited to exact files/lines I read (server.py, backends/base.py, engineering/plan_spec.py, engineering/critique.py, engineering/validator.py, engineering/gear.py, CLAUDE.md). Caveat: a few arXiv IDs returned by search carry future-dated identifiers (e.g. 2602.*, 2603.*, 2605.*, 2606.*) consistent with the environment's stated date of June 2026; I fetched page content rather than relying on the ID alone, but exact quantitative figures inside abstracts were summarized by the WebFetch model and not independently re-verified line-by-line. I did not open every candidate paper from the search lists — I prioritized the ones most directly applicable to this MCP's plan-then-critique + deterministic-snap design." + }, + { + "topic": "Agentic CAD loops and VLM render-critique-repair systems: techniques the AutoCAD MCP Pro project could adopt for autonomous drawing accuracy", + "summary": "The project already implements the *skeleton* of an agentic drawing loop: plan-before-draw (drawing_plan → PlanSpec in engineering/plan_spec.py), deterministic snapping to avoid coordinate hallucination (point_from_snap, server.py:2384), a programmatic critique pass (engineering/critique.py: iso128/layer_color/dim_overlap/untrimmed_corner/duplicate_entities/construction_left), and an 8-step finalize validator (engineering/validator.py). The CRITICAL GAP versus the 2025 literature: the critique/validator loop is 100% symbolic/geometric — it inspects entity lists, layer tables, and endpoint coordinates. The screenshot produced at finalize (server.py:2301-2309) and probed in validator step 7 (validator.py:206-218) is written to disk and checked only for \"are there bytes,\" but is NEVER rendered back into a VLM that compares the drawing to the stated intent (PlanSpec.intent is captured but only replayed symbolically). So the system has \"draw → screenshot → check-pixels-exist\" but not the \"draw → render → VLM-critique → repair\" closed loop that every recent paper (CADFusion, Seek-CAD, CADSmith, Physics-in-the-Loop, See-it-Say-it-Sorted, VIGA) shows is the single biggest lever on autonomous accuracy. The papers converge on a reusable recipe: (1) decompose into Planner/Coder/Executor/Validator/Refiner agents with an inner code-fix loop and an outer geometric/visual validation loop; (2) render to image, have a VLM identify 1-3 grounded discrepancies in QUALITATIVE/relative terms (not pixel coordinates), and feed them back as natural-language critique; (3) generate multiple candidate fixes and have a judge VLM pick the best vs. revert; (4) bound iterations (typically 3-10) with a sliding-window memory; (5) prefer minimal diff edits to avoid corrupting unrelated geometry; (6) where a kernel exists, drive correction from programmatic measurements rather than visual similarity (CADSmith, Physics-in-the-Loop reach 59-87% success). The project's strengths (closed critique enum, deterministic snaps, kernel-style geometric checks) align exactly with the \"programmatic validator\" half of these systems; what is missing is the visual/VLM half and the automated refine-and-judge orchestration. confidence high — WebSearch and WebFetch both available; arXiv IDs in the 2603/2605/2606 range are dated June 2026 (current month per env), so treat those few as very recent / possibly preprint-stage.", + "findings": [ + { + "title": "CADFusion: Text-to-CAD Generation Through Infusing Visual Feedback in LLMs", + "takeaway": "Alternates a Sequential-Learning stage (train on ground-truth parametric sequences for logical coherence) with a Visual-Feedback stage that renders generated sequences and rewards those whose renders are visually preferred while penalizing bad ones — making the rendered image a first-class training/selection signal rather than an afterthought.", + "relevance": "This project renders a screenshot at finalize (server.py:2301-2309) but only checks that bytes exist (validator.py:206-218). CADFusion shows the render should drive a preference/critique signal. The repo could add a drawing_critique_visual focus that renders via the existing view_screenshot path and feeds the PNG to a VLM scored against PlanSpec.intent.", + "source_url": "https://arxiv.org/abs/2501.19054", + "date": "2025-01" + }, + { + "title": "Seek-CAD: Self-refined Generative Modeling for 3D Parametric CAD (DeepSeek-R1, training-free)", + "takeaway": "First to combine VLM visual feedback AND chain-of-thought feedback in a training-free self-refinement loop. Key idea: step-wise visual feedback (SVF) renders not just the final object but INTERMEDIATE construction states, and the VLM critiques the whole sequence so errors are caught mid-build, not only at the end.", + "relevance": "Maps directly onto this project's existing transaction/step model (transaction_begin/commit/rollback) and construction-layer scaffolding. Critique could run per-step (render after each major entity_create batch) instead of only at finalize, catching untrimmed_corner/dim_overlap before they compound. Training-free fits the MCP architecture (no model retraining needed).", + "source_url": "https://arxiv.org/abs/2505.17702", + "date": "2025-05" + }, + { + "title": "CADSmith: Multi-Agent CAD Generation with Programmatic Geometric Validation", + "takeaway": "Five-agent pipeline (Planner → Coder → Executor → Validator → Refiner) with a dual loop: inner loop fixes execution/syntax errors; outer loop measures actual geometry (edge lengths, radii, angles, face counts, hole presence, parallelism/perpendicularity/concentricity) against spec via kernel measurements and routes corrective instructions back to the Coder. Validation is programmatic, not visual.", + "relevance": "This is the closest match to the repo's current design philosophy: engineering/validator.py and engineering/critique.py ARE a programmatic Validator. What's missing is the automated Refiner agent that consumes Issue.handles/detail (plan_spec.py:64-81 already structures these) and emits targeted repair tool-calls. The repo could formalize the Validator→Refiner edge so critique issues auto-generate suggested entity_trim/entity_fillet/dimension_auto fixes.", + "source_url": "https://arxiv.org/pdf/2603.26512", + "date": "2026-03" + }, + { + "title": "Physics-in-the-Loop: Hybrid Agentic Architecture for Validated CAD Engineering Design", + "takeaway": "LangGraph orchestrator with stateful cyclic nodes; three validation tiers (geometric manifold/bounds, FEA structural safety factor, VLM visual intent-match). Agents cannot override FEA results — physics is a deterministic, non-negotiable signal. Reports 81-87% FEA success, 59% hit target safety factor with FEA vs 22% without, converging in 3.3-6.0 iterations.", + "relevance": "Validates the repo's 'non-negotiable closed enum critique' philosophy (CLAUDE.md premium rules) and the 'must return [] before finalize' gate (server.py:2374). Two adoptable patterns: (1) explicit orchestrator state machine with directed cyclic edges instead of the current linear template; (2) bounded iteration budget (3-6) for the repair loop. The FEA tier is out of scope for 2D drafting, but the deterministic-signal-over-VLM-vibes principle is directly applicable to keeping geometric critique authoritative.", + "source_url": "https://arxiv.org/html/2605.19717", + "date": "2026-05" + }, + { + "title": "See it. Say it. Sorted: Agentic System for Compositional Diagram Generation", + "takeaway": "Training-free Critic-Candidates-Judge loop for sketch→SVG. Critic VLM names 1-3 key discrepancies using QUALITATIVE spatial language ('width about half the canvas') not pixel coordinates. Multiple LLMs propose diverse candidate fixes (conservative/moderate/aggressive/alternative/focused). A Judge VLM renders all candidates and picks the best, reverting if none improve. Beat GPT-5 and Gemini-2.5-Pro on structure preservation.", + "relevance": "The qualitative-over-quantitative critique principle is a perfect complement to this project's deterministic point_from_snap (server.py:2384): use snaps for exact coordinates, use VLM only for relative/qualitative judgments it is reliable at. The Candidates+Judge pattern offers a way to make the repair step robust: instead of one blind fix, propose 3 candidate repair plans and judge the re-rendered result. Revert-on-no-improvement maps onto the existing transaction_rollback tool.", + "source_url": "https://arxiv.org/pdf/2508.15222", + "date": "2025-08" + }, + { + "title": "VIGA: Vision-as-Inverse-Graphics Agent via Interleaved Multimodal Reasoning", + "takeaway": "Analysis-by-synthesis loop: generate code → render → verifier inspects from multiple viewpoints using active tools ('zoom out', 'focus on object', 'rotate'), identifies ONE different grounded issue per iteration and ties it back to code parameters; minimal line-level diff edits avoid corrupting other geometry; sliding-window memory; ~10 iterations typical, up to 100 for hard cases.", + "relevance": "Multi-view active inspection maps onto the repo's view_zoom_and_screenshot (server.py:1870) and view_zoom_window — a VLM verifier could request zoom-to-region screenshots to inspect specific Issue.handles regions rather than one global shot. The 'one grounded issue + minimal diff per iteration' discipline prevents the repair step from regressing a clean part of the drawing, complementing the duplicate_entities/untrimmed_corner detectors that already localize issues to specific handles.", + "source_url": "https://arxiv.org/html/2601.11109v2", + "date": "2026-01" + }, + { + "title": "CAD-Coder: Open-Source VLM for CAD Code Generation (GenCAD-Code, 163k image-code pairs)", + "takeaway": "Fine-tuned open VLM that emits editable CadQuery from images; 100% valid-syntax rate, beats GPT-4.5 and Qwen2.5-VL-72B on 3D solid similarity; generalizes to real-world photos. Public weights/dataset at github.com/anniedoris/CAD-Coder.", + "relevance": "Demonstrates that an open, locally-runnable VLM can reliably read a rendered CAD image — meaning the visual-critique tier this project lacks does NOT require a frontier API and could run against the existing ezdxf/matplotlib render output. Could seed a 'reference image → drawing' workflow (user supplies a sketch/photo, agent reproduces it as a dimensioned DXF, then loops to verify visual match).", + "source_url": "https://arxiv.org/abs/2505.14646", + "date": "2025-05" + }, + { + "title": "Fine-Tuning Vision-Language Model for Automated Engineering Drawing Information Extraction (Florence-2 for GD&T)", + "takeaway": "Fine-tunes the open Florence-2 VLM to extract GD&T (geometric dimensioning & tolerancing) and dimension callouts directly from 2D engineering drawings — a verification primitive specialized to drafting rather than 3D solids.", + "relevance": "Directly applicable to a 2D drafting tool: a VLM that reads dimensions/GD&T off the rendered drawing enables a 'round-trip' check — extract the dimensions the VLM SEES in the render and diff them against the dimension_* entities the backend actually created. This would catch dimension_auto mis-placements (dim_overlap focus only checks proximity, not whether the value/witness lines read correctly) and fake-dimension-text (validator.py:75-100 only regex-matches text, can't confirm it visually).", + "source_url": "https://arxiv.org/abs/2411.03707", + "date": "2024-11" + } + ], + "opportunities": [ + "Add a visual critique focus to the closed enum (plan_spec.py CritiqueFocus + critique.py _FOCUS_DISPATCH): render via the existing view_screenshot/view_zoom_and_screenshot path, send PNG + PlanSpec.intent to a VLM, and return Issues with severity/message/handles like every other focus — closing the render→VLM→repair gap that all 7 papers identify as the top accuracy lever.", + "Formalize a Refiner step (CADSmith/Physics-in-the-Loop pattern): consume the structured Issue list (handles + detail already exist in plan_spec.py:64-81) and emit suggested repair tool-calls (entity_trim with keep_x/keep_y for untrimmed_corner, entity_delete for duplicates, dimension_auto for dim_overlap) instead of only reporting. Add a bounded iteration budget (3-6 per Physics-in-the-Loop's 3.3-6.0 convergence).", + "Move critique from finalize-only to per-step / step-wise visual feedback (Seek-CAD SVF): run lightweight critique after each major entity_create batch using the existing transaction model, so untrimmed_corner / dim_overlap are caught before they compound. Render intermediate states for the VLM, not just the final.", + "Adopt qualitative-over-quantitative critique split (See-it-Say-it-Sorted): keep point_from_snap (server.py:2384) authoritative for exact coordinates, restrict the VLM to relative/qualitative judgments (balance, intent-match, view completeness) it is reliable at — and keep geometric/programmatic checks as the non-overridable signal (Physics-in-the-Loop principle), matching the repo's 'non-negotiable closed enum' stance in CLAUDE.md.", + "Add a Candidates+Judge repair pattern: when critique fails, snapshot via transaction_begin, generate 2-3 candidate repair plans, re-render each, let a judge VLM pick the best, and transaction_rollback the losers — robustifying the repair step against single-shot regressions (the rollback infra already exists per backends/base.py _undo_stack).", + "Add multi-view inspection to the verifier (VIGA): drive view_zoom_window to screenshot the bounding region of specific Issue.handles so the VLM critiques localized detail instead of one global low-res shot; report 'one grounded issue + minimal diff' per iteration to avoid regressing clean geometry.", + "Build a dimension round-trip check (Florence-2 GD&T extraction): VLM-extract dimensions visible in the rendered drawing and diff against the actual dimension_* entities the backend created, catching mislabeled/overlapping/missing callouts that the current text-regex check (validator.py:75-100) and proximity-only dim_overlap cannot detect.", + "Add a reference-image-driven workflow (CAD-Coder): let a user supply a sketch/photo as the target, then run the draw→render→VLM-diff-against-target→repair loop until visual match — turning PlanSpec.intent from a text string into a visual contract; CAD-Coder shows an open local VLM suffices, so no frontier-API dependency is forced.", + "Use an open local VLM (CAD-Coder / Florence-2) for the visual tier so the optional visual-critique dependency mirrors the existing extras pattern in CLAUDE.md (e.g. pip install -e \".[vision]\"), keeping the headless ezdxf-only install path unaffected." + ], + "confidence": "High. WebSearch and WebFetch were both available and used (8 searches, 5 full-text fetches). Codebase claims are cited to exact files/lines I read (engineering/critique.py, engineering/plan_spec.py, engineering/validator.py, server.py:2281-2396, 1870, 2384). Caveat: arXiv IDs 2603.26512 (CADSmith), 2605.19717 (Physics-in-the-Loop), 2601.11109 (VIGA) carry 2026 date prefixes matching the env's current month (June 2026) and are likely very recent preprints; their reported numbers (success rates, iteration counts) come from abstract/HTML fetches, not independent verification. CADFusion full-text detail was thin (abstract-level fetch only)." + }, + { + "topic": "CAD MCP competitive survey for AutoCAD MCP Pro positioning", + "summary": "Our backend is entirely 2D. We uniquely lead on engineering rigor and a finalize validator, but the field moved past us on 3D, render-critique loops, and auth.", + "findings": [ + { + "title": "build123d-mcp/agentcad: agent-driven render-measure-critique loop", + "takeaway": "AI creates, renders, measures and fixes errors incrementally; deterministic critique, no human screenshot relay.", + "relevance": "We have view_screenshot (base.py:483) and drawing_critique (base.py:526) but run critique only at finalize; make it in-loop.", + "source_url": "https://github.com/pzfreo/build123d-mcp", + "date": "2026" + }, + { + "title": "FreeCAD Robust MCP: 150+ tools, multi-transport, headless", + "takeaway": "150+ tools with STEP/STL export over three transports; one variant adds screenshots and FEM.", + "relevance": "CAD MCPs are expected to exceed 2D; our HTTP exists but every tool is 2D.", + "source_url": "https://spkane.github.io/freecad-addon-robust-mcp-server/latest/", + "date": "2026" + }, + { + "title": "Blender MCP ~22k stars: code exec plus asset integrations", + "takeaway": "Small core plus arbitrary Python exec; adoption driven by asset and text-to-3D integrations.", + "relevance": "Code escape hatch is near-universal; we only have COM-only run_command/run_lisp (server.py:2018) and no symbol library.", + "source_url": "https://github.com/ahujasid/blender-mcp", + "date": "2026" + }, + { + "title": "RhinoMCP: code escape hatch, viewport capture, document context", + "takeaway": "Runs Rhino/Grasshopper with vision feedback and document context reading.", + "relevance": "Mirror viewport-in-loop and document-as-resource (autocad resources server.py:2490); their parametric niche affirms our 2D edge.", + "source_url": "https://github.com/jingcheng-chen/rhinomcp", + "date": "2026" + }, + { + "title": "Snyk roundup of nine CAD MCP servers", + "takeaway": "Vision only in FreeCAD/Blender/Rhino; design validation in none; HTTP auth only in Onshape and Fusion.", + "relevance": "Confirms our moat; lead with the only CAD MCP with a design-rule validation gate (finalize + critique + ISO).", + "source_url": "https://snyk.io/articles/9-mcp-servers-for-computer-aided-drafting-cad-with-ai/", + "date": "2026" + }, + { + "title": "Fusion 360 and Onshape MCPs: 3D parametric plus REST/OAuth", + "takeaway": "Fusion does extrude/revolve with STEP export; Onshape 18 tools over REST with keys; APS uses OAuth.", + "relevance": "We lack 3D solids and authenticated remote transport; HTTP has no auth and ships dangerous_commands_enabled (server.py:111).", + "source_url": "https://mcpmarket.com/server/fusion-360", + "date": "2026" + } + ], + "opportunities": [ + "OWN THE MOAT: market the only CAD MCP with a design-rule validation gate; surface drawing_critique (base.py:526) and drawing_finalize (server.py:2281).", + "CLOSE THE FEEDBACK LOOP: make critique iterative - draw, screenshot (server.py:1866), critique, fix, repeat.", + "ADD A 3D SOLIDS TIER: extrude/revolve from 2D profiles plus booleans on COM and STEP/STL export on ezdxf.", + "STANDARD-PARTS LIBRARY: ship ISO/DIN fastener and symbol blocks via a library_insert tool using block_insert (server.py:1279).", + "AUTHENTICATED REMOTE TRANSPORT: add API-key middleware to the HTTP transport given dangerous_commands_enabled (server.py:111).", + "GUARDED SCRIPTING ESCAPE HATCH: add an opt-in sandboxed ezdxf-side scripting tool, off by default.", + "RESOURCES AS A SELLING POINT: promote autocad resources (server.py:2490-2613) and add a geometry-summary resource." + ], + "confidence": "WebSearch/WebFetch used; repo grounded via Read/Grep/Glob on base.py and server.py. Competitor counts as of June 2026 may have drifted. 2D-only finding is high-confidence from grep of the backend ABC." + }, + { + "topic": "Best-in-class text-to-CAD / AI-CAD landscape vs. AutoCAD MCP Pro: UX, accuracy techniques, and feature sets to match or beat", + "summary": "The leading AI-CAD products and 2025-2026 research converge on four pillars this repo only partially implements. (1) ACCURACY VIA CLOSED-LOOP VERIFICATION: the frontier is not single-pass generation+critique but iterative generate-execute-measure-refine loops. CADSmith (5-agent Planner/Coder/Executor/Validator/Refiner) reports a 38x Chamfer-distance improvement (28.37mm -> 0.74mm) and IoU 0.81 -> 0.96 by combining exact OpenCASCADE kernel measurements (volume, bbox, face/edge/vertex counts, watertightness) with an INDEPENDENT vision-language-model Judge; removing the rendered-image input alone degraded hard-case Chamfer 35x, proving numeric checks miss \"false convergence.\" This repo's drawing_critique (engineering/critique.py) is the right instinct but is single-pass, 2D-only, and purely numeric/heuristic — no VLM judge, no measured-vs-target feedback, no refine loop. (2) PROACTIVE CLARIFICATION before drawing: ProCAD asks targeted clarifying questions when prompts are under-specified or self-conflicting, cutting invalidity 14.6% -> 0.9% and Chamfer ~80%. This repo's drawing_plan (engineering/plan_spec.py PlanSpec) commits intent but never detects ambiguity or asks. (3) PARAMETRIC, EDITABLE OUTPUT with post-hoc dimension sliders (Adam, Zoo) — users tweak dimensions without redrawing; this repo emits flat DXF entities with no parametric/constraint model. (4) DESIGN-INTENT EVALUATION beyond shape match: MUSE grades manufacturability/functionality/assemblability via rubric VLM judge; the validator.py heuristics (regex for fake dims, title-string checks) are far below this bar. The repo's genuine differentiators worth doubling down on: a real CAD backend (live AutoCAD COM + ezdxf) that most text-to-CAD tools lack, ISO-128 lineweight/layer discipline, and the snap-not-guess rule (point_from_snap) — which directly counters the #1 LLM CAD failure mode (hallucinated coordinates). Net: the repo is architecturally ahead on backends and standards but behind on the verification loop, VLM-grounded critique, clarification, and editability that define leaders.", + "findings": [ + { + "title": "Zoo.dev Text-to-CAD (ML-ephant API)", + "takeaway": "Generates editable B-Rep CAD (not meshes) from text + target file format; most calls finish in 10-30s. Accuracy is highly prompt-specificity-dependent: specific dimensioned prompts ('10cm helical gear, 20 teeth') succeed; vague prompts fail. Open-source, multi-language, GPU-native geometry engine.", + "relevance": "Confirms (a) editable/parametric output is table stakes and (b) prompt specificity drives accuracy — validating this repo's drawing_plan intent-commit step, but exposing that the repo has no mechanism to PUSH BACK on vague prompts (see ProCAD).", + "source_url": "https://zoo.dev/machine-learning-api", + "date": "2025" + }, + { + "title": "CADSmith: Multi-Agent CAD Generation with Programmatic Geometric Validation", + "takeaway": "5 agents (Planner/Coder/Executor/Validator/Refiner) with dual correction loops: inner loop fixes execution errors via RAG over API docs + error patterns (3 retries); outer loop runs an INDEPENDENT Claude-Opus VLM Judge fed the prompt, code, EXACT kernel measurements (volume/bbox/face-edge-vertex counts/center-of-mass/solid validity) AND 3-view renders (5 iterations). Results: exec success 95%->100%, IoU 0.81->0.96, mean Chamfer 28.37mm->0.74mm (38x). Removing renders alone: hard-case Chamfer 1.42->49.68mm (35x worse). Refiner gets exact numeric discrepancies, never ungrounded prose.", + "relevance": "This is the single highest-leverage upgrade: turn drawing_critique into a closed refine loop and add a VLM-judge over the existing screenshot pipeline (view_screenshot already exists). The repo already produces a screenshot at finalize — feed it to a judge alongside measured-vs-PlanSpec deltas instead of discarding it. RAG-over-docs (vs fine-tune) also fits an MCP server better.", + "source_url": "https://arxiv.org/abs/2603.26512", + "date": "2026" + }, + { + "title": "Clarify Before You Draw (ProCAD): Proactive Agents for Robust Text-to-CAD", + "takeaway": "A clarifier agent audits the prompt for under-specification and internal conflicts, asks all targeted questions in ONE message, then emits a corrected spec for the coder. On 2,469 ambiguous prompts: invalidity 14.6%->0.9%, mean Chamfer ~80% lower. Ambiguity training data made by perturbing verified specs and keeping only perturbations that caused >=10x Chamfer degradation.", + "relevance": "drawing_plan (plan_spec.py) currently accepts whatever intent string it's given. Adding a pre-plan ambiguity/conflict check that returns clarifying questions (missing dimensions, conflicting features) before any entity_create_* would directly raise output validity — and fits the existing 'plan before draw' rule in CLAUDE.md.", + "source_url": "https://arxiv.org/html/2602.03045v1", + "date": "2026" + }, + { + "title": "From Intent to Execution: Multimodal Chain-of-Thought RL for Precise CAD Code Generation", + "takeaway": "Combines multimodal chain-of-thought (visual+text intermediate reasoning), RL reward signals, and geometric feedback loops with self-refinement. Establishes that intermediate reasoning steps + geometric validation beat direct end-to-end translation for precision.", + "relevance": "Supports adding an explicit planning/reasoning trace and geometric reward signal. For an MCP (inference-time, not training) the actionable parallel is: structured intermediate steps + a geometric reward/score surfaced back to the model each loop, which the repo's critique could compute as a single scalar 'drawing score'.", + "source_url": "https://arxiv.org/pdf/2508.10118", + "date": "2025" + }, + { + "title": "Adam (AdamCAD) — AI CAD Copilot, YC W25, $4.1M seed", + "takeaway": "Text-to-parametric-3D in seconds with post-generation DIMENSION SLIDERS (edit without redrawing); part-level conversational selection (click a feature, talk to it); enterprise copilot integrating Onshape/PLM/suppliers; built-in 'skills' for CAD, sourcing, and engineering documentation; applies one change across many CAD files.", + "relevance": "Two transferable UX ideas: (1) parametric editability — expose drawing parameters the user can tweak post-generation rather than flat DXF; (2) entity-level conversational editing — 'select this circle, make it the bore' maps onto the repo's handle-based model. The 'skills' framing already mirrors this repo's .claude/skills approach.", + "source_url": "https://techcrunch.com/2025/10/31/yc-alum-adam-raises-4-1m-to-turn-viral-text-to-3d-tool-into-ai-copilot/", + "date": "2025-10-31" + }, + { + "title": "DraftAid — 3D-to-2D production-drawing automation", + "takeaway": "Auto-converts 3D models into production-ready 2D drawings with consistent dimensions, views, and drafting standards baked in; integrates SolidWorks/Inventor; claims up to 90% drawing-time reduction. Wins the most time-intensive CAD task: fabrication drawings.", + "relevance": "Directly adjacent to this repo's 2D engineering output. The standards-enforcement + auto-dimensioning + multi-view layout is exactly what dimension_auto and titleblock tools aim at — but DraftAid's auto-view-generation and full standards pass set the bar the repo's validator/critique should aspire to.", + "source_url": "https://thecadhub.com/blog/ai-cad-software-in-2025-adamcad-cadgpt-draftaid/", + "date": "2026" + }, + { + "title": "CADGPT — in-tool AutoCAD copilot", + "takeaway": "Context-aware chat embedded in the CAD UI that writes AutoLISP/Python automation and understands CAD-specific commands/terminology; augments rather than replaces the drafter.", + "relevance": "Validates this repo's COM-only system_run_lisp / system_run_command surface as a real product category. A safe, sandboxed LISP/script-generation path (the repo already has security.py) is a competitive feature, not just plumbing.", + "source_url": "https://thecadhub.com/blog/ai-cad-software-in-2025-adamcad-cadgpt-draftaid/", + "date": "2026" + }, + { + "title": "Autodesk Project Bernini + 'neural CAD' + Autodesk Assistant", + "takeaway": "Bernini generates FUNCTIONAL 3D geometry (shape and texture decoupled) from text/image/voxel/point-cloud, trained on 10M shapes; 'neural CAD' foundation models aim to automate 80-90% of routine design; Autodesk Assistant (coming to AutoCAD/Revit/Inventor) demoed AUTOMATED COMPLIANCE/STANDARDS CHECKING in AutoCAD.", + "relevance": "The incumbent is shipping an in-AutoCAD AI assistant whose flagship demo is automated compliance checking — exactly the niche of this repo's drawing_critique/validator. To 'beat' it the repo should make standards compliance (ISO-128, layer/lineweight, GD&T) its sharpest, most-cited capability.", + "source_url": "https://aecmag.com/features/autodesk-shows-its-ai-hand/", + "date": "2025" + }, + { + "title": "MUSE: Benchmarking Manufacturable, Functional, and Assemblable Text-to-CAD", + "takeaway": "Argues geometric-similarity metrics miss real design quality; introduces a 3-stage protocol (code check -> geometric check -> design-intent alignment) where the final stage uses design-specific rubrics + a rubric-based VLM judge (human-validated) to score functionality, manufacturability, assemblability.", + "relevance": "A blueprint for upgrading validator.py: replace ad-hoc regex/title heuristics with a staged protocol whose final stage is a rubric VLM judge over the rendered drawing. Also defines what 'good' means beyond 'it drew lines.'", + "source_url": "https://arxiv.org/html/2605.28579", + "date": "2026" + }, + { + "title": "Text-to-CAD evaluation metric suite (CadBench/BenchCAD/surveys)", + "takeaway": "Standard metric suite: volumetric IoU, surface IoU, Chamfer distance, valid-shape rate (Invalidity Ratio = % that can't execute/render), plus token/operation counts; surface-IoU match threshold ~1% of bbox diagonal. Validity and design-intent alignment are emphasized over pure shape resemblance.", + "relevance": "The repo has no quantitative quality metric — drawing_critique returns a list of issues but no score. Adopting an Invalidity-Ratio-style pass/fail plus a 2D analog of IoU/Chamfer (against the PlanSpec or a reference DXF) gives a regression-testable accuracy number for the smoke suite (scripts/smoke_premium.py).", + "source_url": "https://arxiv.org/html/2605.10873v1", + "date": "2026" + } + ], + "opportunities": [ + "Close the loop: convert drawing_critique (engineering/critique.py) from single-pass into an iterative generate->measure->refine cycle, returning measured-vs-PlanSpec numeric deltas (not just issue strings) so the model can self-correct — mirrors CADSmith's outer loop that drove Chamfer 28.37->0.74mm.", + "Add a VLM-judge focus to drawing_critique that consumes the already-produced view_screenshot bytes + the PlanSpec + measured geometry and judges intent-match. CADSmith showed dropping the visual channel made hard cases 35x worse; the repo already renders a screenshot at finalize but throws away its judgment value.", + "Add a pre-plan clarification step to drawing_plan (plan_spec.py): detect under-specified/conflicting intent and return targeted clarifying questions in one batch before any entity_create_* runs (ProCAD: invalidity 14.6%->0.9%).", + "Introduce a single scalar 'drawing score' / Invalidity-Ratio pass-fail from critique+validator so quality is regression-testable in scripts/smoke_premium.py — today there is no quantitative accuracy metric anywhere in the repo.", + "Make standards-compliance the flagship capability (Autodesk's own AutoCAD-AI demo is compliance checking): broaden critique beyond the 6 closed focuses to GD&T placement, datum/feature-control-frame validity, drawing-frame/scale consistency, and view-projection checks — areas no surveyed text-to-CAD product covers for 2D.", + "Add parametric editability: capture drawing parameters (driving dimensions) in the PlanSpec and expose a tool to re-drive them so users tweak a value and regenerate, matching Adam/Zoo's editable-output expectation instead of emitting flat one-shot DXF.", + "Add entity-level conversational editing leveraging the existing handle model ('select this circle -> make it the 20mm bore'), the UX paradigm Adam highlights, using entity_select_smart + point_from_snap which already exist.", + "Adopt RAG-over-docs for error recovery rather than relying on model memory: index ezdxf/AutoCAD-COM error patterns and tool-usage docs so failed tool calls get retried with grounded fixes (CADSmith inner loop, 3 retries) — fits an MCP server better than fine-tuning.", + "Generalize the 8-step validator.py beyond gear/keyway-specific regex/title heuristics into a MUSE-style staged protocol (code/exec check -> geometric check -> rubric VLM intent check) so it scales to arbitrary drawings, not just the engineering primitives currently hardcoded.", + "Double down on the snap-not-guess rule (point_from_snap) and surface it as a marketed accuracy feature: hallucinated coordinates are the dominant LLM-CAD failure mode the literature works hard to fix, and the repo already structurally prevents it — make critique flag any geometry whose endpoints were not snap-derived." + ], + "confidence": "High. WebSearch and WebFetch were both available and used across 7 searches + 4 fetches. All product/research claims are cited to primary sources (arXiv papers, vendor pages, TechCrunch, AEC Magazine). Repo gap claims are grounded in files I read directly: engineering/critique.py (single-pass, 2D-only, numeric/heuristic, 6 closed focuses), engineering/plan_spec.py (PlanSpec commits intent, no ambiguity detection), engineering/validator.py (regex/title-string heuristics, gear-specific), and server.py tool registrations (point_from_snap, dimension_auto, entity_select_smart, view_screenshot exist). One arXiv abstract (multimodal CoT RL, 2508.10118) returned method-level detail but compressed metrics; treated qualitatively. Several arXiv IDs carry 2026 dates consistent with the stated June 2026 current date." + }, + { + "topic": "AutoCAD COM/ActiveX automation API surface vs. com_backend.py — under-used high-value capabilities", + "summary": "com_backend.py (1661 lines) exercises only a narrow slice of the AutoCAD ActiveX object model. Every entity creation call goes through `_msp()` → `_acad_doc().ModelSpace` (com_backend.py:125-127), so the backend is effectively ModelSpace-only. The ActiveX ModelSpace/PaperSpace/Block collections expose 60+ Add* methods; the backend wires up ~12 (AddLine, AddCircle, AddArc, AddLightWeightPolyline, AddText, AddMText, AddHatch, AddSpline, AddEllipse, AddPoint, InsertBlock) plus 5 dimension adds and ArrayRectangular/ArrayPolar. The largest, highest-value gaps for an *engineering drawing* product are: (1) PAPER SPACE / LAYOUTS + PLOT — there is no layout abstraction at all; `drawing_export_pdf` (com_backend.py:528-534) blindly calls `doc.Plot.PlotToFile(path, \\\"DWG To PDF.pc3\\\")` with no PlotConfiguration / page setup / layout selection / multi-sheet publish, so it cannot honor the A3/scale targets the premium workflow (drawing_plan sheet_size/scale) promises. (2) TABLE objects (AddTable) for BOM / revision / parts tables — absent; users must hand-draw grids. (3) MLEADER (AddMLeader) for ISO leader notes — absent; CLAUDE.md's premium rules even reference \\\"leader notes\\\" but no tool creates them. (4) FIELDs — no field/title-block auto-population, so titleblock text is static. (5) 3D SOLIDS / REGIONS (AddBox/Cylinder/ExtrudedSolid/RevolvedSolid + Boolean + AddRegion) — system_status advertises \\\"all_entity_types\\\" (com_backend.py:1443) which is false. (6) XREF (AttachExternalReference). (7) DYNAMIC BLOCK properties (GetDynamicBlockProperties). (8) Named UCS (UserCoordinateSystems.Add) — needed for the construction-geometry / multi-view workflow. (9) DIMENSION STYLES (DimStyles.Add + CopyFrom) — dimensions are created with whatever the current style is; no ISO-129 dimstyle is ever set, undercutting the \\\"Dimensions are ISO 129\\\" rule in CLAUDE.md. (10) GRADIENT HATCH (AddHatch with acGradientObject) and MTEXT inline formatting. (11) Real SelectionSet filter API is used in only two places (analysis_select_by_layer com_backend.py:1333-1350, analysis_entities_in_region :1286-1308); entity_list (:967-999) and analysis_select_by_type (:1352-1367) iterate every ModelSpace item in Python instead of pushing a DXF group-code filter into AutoCAD, which the web sources flag as the #1 COM performance killer. WebSearch + WebFetch were available and used; all API signatures below are confirmed against Autodesk ActiveX reference pages.", + "findings": [ + { + "title": "PaperSpace Collection — AddPViewport / Layout association (ActiveX)", + "takeaway": "PaperSpace is a Block-like collection exposing AddPViewport(center, width, height) for floating viewports, plus all the same Add* geometry/annotation methods as ModelSpace, and it owns a Layout object holding every plot setting (page size, plot device, scale, plot area). Layouts collection + PlotConfiguration objects store named page setups.", + "relevance": "The backend has zero paper-space support (every create call hits `_msp()` ModelSpace, com_backend.py:125-127). To produce real plotted A3 sheets at a declared scale (the contract of drawing_plan(sheet_size,scale) and titleblock_apply_iso_a3), you must create/select a Layout, set its CanonicalMediaName + StandardScale/CustomScale, drop an AddPViewport, and lock its scale. Add a `layout_*` family (layout_create/list/set_active, viewport_create, viewport_set_scale).", + "source_url": "https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-ActiveX-Reference/files/GUID-C5E998B5-6EB1-459A-98B1-444C74DB4516.htm", + "date": "2024" + }, + { + "title": "Layout / PlotConfiguration + Document.Plot.PlotToFile / PlotToDevice (ActiveX)", + "takeaway": "The Plot object supports PlotToFile(filename, [printConfig]) and PlotToDevice, but correct output requires first configuring the Layout/PlotConfiguration (ConfigName, CanonicalMediaName, PlotType, PlotRotation, StandardScale, PlotWithLineweights). Multi-sheet PDF is done by iterating layouts (SetLayoutsToPlot) or via the Sheet Set / PUBLISH path.", + "relevance": "`drawing_export_pdf` (com_backend.py:528-534) calls PlotToFile with only the device string and no page setup — output paper size, scale, and which space (model vs layout) are entirely unspecified and non-deterministic. This silently ignores the scale/sheet the premium workflow set up. Fix: build a plot config (or apply a saved page setup) honoring drawing_plan's sheet_size/scale before plotting; add multi-layout publish.", + "source_url": "https://keanw.com/2007/09/driving-a-multi.html", + "date": "2007" + }, + { + "title": "AddTable Method (ActiveX)", + "takeaway": "ModelSpace/PaperSpace.AddTable(insertionPoint, numRows, numColumns, rowHeight, colWidth) returns a Table object; cells are filled with SetText(row,col,string), SetCellValue, merged, and styled via a TableStyle. Tables are first-class AcDbTable entities.", + "relevance": "Engineering deliverables need BOM / parts-list / revision tables. Currently users would have to hand-draw line grids with entity_create_line and place text cell-by-cell — error-prone and not editable. A `table_create` + `table_set_cell` tool (plus a `bom_from_blocks` meta-tool counting block refs) is a high-value, low-risk add that fits the 'meta-tools over raw' rule in CLAUDE.md.", + "source_url": "https://help.autodesk.com/cloudhelp/2015/DEU/AutoCAD-ActiveX/files/GUID-A5A767A8-98D0-40B4-89D1-1F0E5AC52EB7.htm", + "date": "2015" + }, + { + "title": "AddMLeader Method (ActiveX)", + "takeaway": "ModelSpace.AddMLeader(pointsArray, leaderLineIndex) creates a multileader; content (MText or block) and arrowheads are set via the MLeaderStyle and the object's TextString/ContentBlockName properties. Modern ISO leader notes are MLEADER, not the legacy AddLeader.", + "relevance": "CLAUDE.md's premium dimension rule explicitly carves out 'leader notes' as a valid manual case, but no tool can create one. Adding `mleader_create(points, text, style)` lets the agent place balloon callouts / weld notes / surface-finish flags properly instead of faking them with separate line+text entities (which drawing_critique would also mis-count).", + "source_url": "https://help.autodesk.com/cloudhelp/2016/CHS/AutoCAD-ActiveX/files/GUID-991E00FE-2128-4162-9C4A-80934C4897B2.htm", + "date": "2016" + }, + { + "title": "3D Solids: AddBox/AddCylinder/AddExtrudedSolid/AddRevolvedSolid/AddSphere/AddTorus/AddWedge + Boolean + AddRegion (ActiveX)", + "takeaway": "ModelSpace exposes AddBox(center,L,W,H), AddCylinder(center,r,h), AddExtrudedSolid(region,height,taper), AddRevolvedSolid(region,axisPt,axisDir,angle), and Boolean(acUnion|acSubtraction|acIntersection) for combining; AddRegion(objectArray) builds 2D regions from closed curves. Windows-only (not LT).", + "relevance": "system_status reports capability 'all_entity_types' (com_backend.py:1443) but the backend creates no 3D solids, no regions, and no booleans — the claim is inaccurate. If 3D/MCAD output is in scope, a `solid_*`/`region_*` family unlocks an entire dimension of the product; if not, the advertised capability string should be corrected to avoid misleading agents.", + "source_url": "https://documentation.help/AutoCAD-ActiveX-AAG/WS1a9193826455f5ff1a32d8d10ebc6b7ccc-6d32.htm", + "date": "n/a" + }, + { + "title": "AttachExternalReference Method (ActiveX)", + "takeaway": "object.AttachExternalReference(PathName, Name, InsertionPoint, XScale, YScale, ZScale, Rotation, Overlay[, Password]) attaches/overlays an xref and returns an ExternalReference object; Detach/Reload/Bind/Unload manage its lifecycle. Confirmed signature.", + "relevance": "No xref support exists (base.py has no xref_* methods). Real drawing sets reference standard borders/details. Adding `xref_attach/detach/reload/bind` lets the agent compose drawings from shared title blocks and detail libraries instead of copying geometry, and keeps file sizes small.", + "source_url": "https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-ActiveX-Reference/files/GUID-28CB9C44-08E6-47EF-A982-8F505D8C68F6.htm", + "date": "2022" + }, + { + "title": "GetDynamicBlockProperties / DynamicBlockReferenceProperty Object (ActiveX)", + "takeaway": "BlockReference.GetDynamicBlockProperties() returns an array of DynamicBlockReferenceProperty objects, each with PropertyName, Value (settable), AllowedValues, and IsDynamicBlock on the reference. This is how you drive parametric blocks (e.g. adjustable beam length, door swing).", + "relevance": "block_insert/block_get_attributes (com_backend.py:1195-1251) only handle attribute text, not dynamic parameters. Engineering symbol libraries (valves, fasteners, structural sections) are almost always dynamic blocks. Add `block_get_dynamic_props`/`block_set_dynamic_prop` so the agent can size/configure inserted parametric blocks.", + "source_url": "https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-ActiveX/files/GUID-ED753239-0C53-4741-BE13-99B21751C22F.htm", + "date": "2015" + }, + { + "title": "DimStyles.Add + DimStyle.CopyFrom (ActiveX)", + "takeaway": "Create a dimension style with Document.DimStyles.Add(name); set all the DIMxxx system variables on the document, then call dimStyle.CopyFrom(doc) to bake the current dim variable state into the named style. ActiveDimStyle selects it for subsequent dims.", + "relevance": "All dimension_* methods (com_backend.py:740-811) create dims under whatever DimStyle is current and never establish an ISO-129 style — directly undercutting CLAUDE.md's 'Dimensions are ISO 129' rule and dimension_auto's intent. Add `dimstyle_create_iso` (set DIMTXT/DIMASZ/DIMDEC/DIMTOFL/etc., CopyFrom, make active) and call it from drawing_apply_iso_layers so dims are standards-compliant by construction.", + "source_url": "https://documentation.help/AutoCAD-ActiveX-AAG/WS1a9193826455f5ff1a32d8d10ebc6b7ccc-6b5e.htm", + "date": "n/a" + }, + { + "title": "Fields (AcDbField) in MText / Attributes — FieldDisplay (ActiveX/.NET)", + "takeaway": "Field expressions (e.g. %<\\AcVar Filename>%, %<\\AcObjProp ...>%, %<\\AcDiesel ...>%) embedded in MText/Attribute strings auto-update. They live in an AcDbField in the object's extension dictionary; FieldDisplay sysvar toggles the gray background. Set via the text string itself plus a regen/UpdateField.", + "relevance": "Title-block text (date, sheet number, scale, filename, drawing area) is static today (titleblock_apply_iso_a3 just writes text). Supporting field expressions in entity_create_mtext / a `field_insert` tool makes title blocks and dynamic notes self-updating — a hallmark of professional drawings and a differentiator over hand-placed text.", + "source_url": "https://www.keanw.com/2007/07/accessing-the-a.html", + "date": "2007" + }, + { + "title": "AddHatch with acGradientObject — gradient fills (ActiveX)", + "takeaway": "AddHatch(PatternType, PatternName, Associative[, HatchObjectType]) accepts HatchObjectType=acGradientObject to produce a gradient; GradientName ('LINEAR','SPHERICAL', etc.), GradientColor1/GradientColor2 (AcCmColor via SetRGB), and GradientAngle control it. AppendInnerLoop adds islands.", + "relevance": "entity_create_hatch (com_backend.py:651-676) hardcodes acPatternTypePreDefined=0, single outer loop, no inner loops (islands) and no gradient. Sectional engineering hatches with holes need AppendInnerLoop; presentation/architectural fills need gradients. Extend the hatch tool with island loops + a gradient mode.", + "source_url": "https://help.autodesk.com/cloudhelp/2019/DEU/AutoCAD-ActiveX-Reference/files/GUID-BAF2BEDD-489A-42D1-92B1-698DC9DAE675.htm", + "date": "2019" + }, + { + "title": "UserCoordinateSystems.Add (named UCS) + ActiveUCS (ActiveX)", + "takeaway": "Document.UserCoordinateSystems.Add(origin, xAxisPnt, yAxisPnt, name) creates a named UCS; setting doc.ActiveUCS makes it current so subsequent coordinates are relative to it. UCSs persist in the drawing.", + "relevance": "Multi-view drawings (front/top/side, auxiliary views, section planes) are far easier to place with named UCS origins than absolute-coordinate math. drawing_plan already takes view_count; a `ucs_*` family would let each view live in its own coordinate frame, complementing point_from_snap and reducing coordinate-guessing.", + "source_url": "https://help.autodesk.com/cloudhelp/2024/KOR/AutoCAD-ActiveX-Reference/files/GUID-1121D09A-27D8-4289-8D8C-03A3339D95BA.htm", + "date": "2024" + }, + { + "title": "SelectionSet.Select/SelectAll with DXF group-code filters (ActiveX) + COM perf guidance", + "takeaway": "SelectionSet supports Select(mode, pt1, pt2, filterType, filterData) and SelectAll(filterType, filterData) where filterType is a short-int array of DXF group codes (0=type, 8=layer, 62=color, -4 for logical operators) and filterData the matching values — letting AutoCAD do the filtering. Web sources stress that per-entity COM property reads in a Python loop are the dominant performance cost; minimize round-trips.", + "relevance": "entity_list (com_backend.py:967-999) and analysis_select_by_type (:1352-1367) loop over every ModelSpace.Item(i) in Python and read .ObjectName/.Layer per entity — O(n) COM round-trips that get slow on large drawings. analysis_select_by_layer (:1333-1350) already shows the right pattern (group code 8). Push type/layer/color filters into SelectAll filter arrays so AutoCAD returns only matches, and back entity_select_smart with the same mechanism.", + "source_url": "https://adndevblog.typepad.com/autocad/2012/07/vba-how-to-setup-selectionset-filters-for-a-block-or-layer.html", + "date": "2012" + } + ], + "opportunities": [ + "Layout/PaperSpace family (layout_create/list/set_active, viewport_create via AddPViewport, viewport_set_scale + lock) so the A3/scale promised by drawing_plan/titleblock_apply_iso_a3 actually drives a plotted sheet — every create call currently hits ModelSpace only (com_backend.py:125-127).", + "Make drawing_export_pdf deterministic: configure the Layout/PlotConfiguration (CanonicalMediaName, StandardScale/CustomScale, PlotType, PlotWithLineweights) from the active PlanSpec before PlotToFile, instead of the bare `doc.Plot.PlotToFile(path,'DWG To PDF.pc3')` at com_backend.py:528-534. Add multi-layout publish (SetLayoutsToPlot / PUBLISH) for multi-sheet PDFs.", + "table_create + table_set_cell (AddTable) and a bom_from_blocks meta-tool, replacing hand-drawn grid lines for BOM/parts/revision tables.", + "mleader_create (AddMLeader) for ISO leader notes / balloons / weld & surface-finish callouts — CLAUDE.md already reserves 'leader notes' as a manual dimension case but no tool exists.", + "dimstyle_create_iso (set DIMTXT/DIMASZ/DIMDEC/DIMTOFL..., DimStyles.Add + CopyFrom, set ActiveDimStyle) wired into drawing_apply_iso_layers so dimension_* output actually satisfies the ISO-129 rule instead of inheriting an arbitrary current style.", + "Field support: allow field expressions in entity_create_mtext and/or a field_insert tool so title-block date/sheet/scale/filename/area auto-update; expose FieldDisplay toggle. Makes titleblock_apply_iso_a3 dynamic rather than static text.", + "xref_attach/detach/reload/bind/unload (AttachExternalReference — confirmed signature) so drawings compose from shared borders/detail libraries instead of copied geometry.", + "block dynamic-property access (GetDynamicBlockProperties → block_get_dynamic_props / block_set_dynamic_prop) to drive parametric symbol libraries; block_insert today only handles attribute text (com_backend.py:1195-1251).", + "Push selection into AutoCAD: rewrite entity_list (com_backend.py:967-999) and analysis_select_by_type (:1352-1367) to use SelectionSet.SelectAll with DXF group-code filter arrays (the pattern analysis_select_by_layer already uses at :1339) — eliminates O(n) per-entity COM round-trips on large drawings and gives entity_select_smart a real backend.", + "Extend entity_create_hatch (com_backend.py:651-676) with inner-loop islands (AppendInnerLoop) for sectioned parts with holes and an acGradientObject gradient mode for presentation fills.", + "Named UCS family (ucs_create via UserCoordinateSystems.Add + set ActiveUCS) to anchor each of drawing_plan's view_count views in its own coordinate frame, reducing absolute-coordinate math alongside point_from_snap.", + "3D solids/regions/booleans (AddBox/AddCylinder/AddExtrudedSolid/AddRevolvedSolid/AddRegion + Boolean) IF 3D is in scope — and either implement it or correct the misleading 'all_entity_types' capability advertised by system_status (com_backend.py:1443), which is currently false." + ], + "confidence": "Medium-high. WebSearch and WebFetch were both available and used; all 12 ActiveX API signatures/objects cited were verified against official Autodesk help pages or the AutoCAD ActiveX Developer's Guide, and AttachExternalReference's exact parameter list was fetched directly. com_backend.py and backends/base.py were read in full, so the 'what we use today' side is from source, not inference. Lower confidence on the exact AutoCAD plot/PlotConfiguration property names per version (these shift slightly across releases and were sourced partly from a 2007 .NET blog), and on whether 3D is in product scope (a roadmap decision, not a code fact)." + }, + { + "topic": "ezdxf 1.3 plus 1.4 capability map versus autocad-mcp EzdxfBackend under-used features", + "summary": "EzdxfBackend in backends ezdxf_backend.py is 2051 lines and uses only a thin slice of the ezdxf 1.3 plus and 1.4 API. A Grep over the backends folder for set_gradient, set_solid_fill, paperspace, add_viewport, odafc, export_dwg, groupby, dimstyles, MTextEditor, add_ordinate, and qsave returned zero matches. Key gaps with line references in backends ezdxf_backend.py. Hatch is pattern only with a single closed polyline path at lines 593 to 606. Dimensions never set a dimension style or override dict so all dims use the ezdxf default style at lines 668 to 774, and the dimension_auto ordinate style fakes it with two linear dims at lines 1988 to 2004. Rendering uses raw matplotlib with no Configuration and no SVG or PyMuPDF backend at lines 357 to 379 and 1400 to 1424. There is no paperspace or viewport support and view zoom methods are no ops at lines 1394 to 1398. DWG export is faked because drawing_save_as accepts the dwg format but just calls doc saveas writing DXF bytes at lines 345 to 355. Querying is hand rolled with Python loops instead of the ezdxf query method and groupby at lines 990 to 1011, 1293 to 1309, 1371 to 1390, and 2008 to 2050. Audit reports errors but never calls the auditor fix method and drawing_open has no recover fallback at lines 448 to 457 and 309 to 331. MTEXT uses a plain string with no MTextEditor at lines 578 to 591. Offset supports only LINE and CIRCLE at lines 868 to 908, and trim, extend, fillet, and chamfer support only LINE plus LINE at lines 1535 to 1750.", + "findings": [ + { + "title": "Drawing add-on Configuration and SVG and PyMuPDF backends are unused", + "takeaway": "ezdxf addons drawing exposes MatplotlibBackend, PyMuPdfBackend, native SVGBackend, PlotterBackend, and DXFBackend. matplotlib qsave takes bg fg dpi and config. Configuration controls lineweight_scaling, min_lineweight, background_policy, color_policy, lineweight_policy ABSOLUTE or RELATIVE, hatch_policy, and line_policy. layout Page Margins and Settings set scale, fit_page, crop_at_margins, output_layers, and Units mm. draw_layout can render any paperspace layout.", + "relevance": "drawing_export_pdf lines 357 to 379 and view_screenshot lines 1400 to 1424 hardcode matplotlib with no Configuration and no paperspace. Adopting Configuration plus SVGBackend or PyMuPdfBackend gives true ISO lineweights, white backgrounds, vector output, and layout specific rendering.", + "source_url": "https://ezdxf.mozman.at/docs/addons/drawing.html" + }, + { + "title": "Hatch supports gradients, solid fills, edge paths, islands, and associativity", + "takeaway": "Hatch supports set_solid_fill with color style and rgb, set_gradient with color1 color2 rotation centered one_color tint and a name such as LINEAR or SPHERICAL, set_pattern_definition for custom patterns, BoundaryPaths add_edge_path with line arc ellipse and spline edges, islands via path_type_flags, and associate. ISO and ANSI pattern names work in set_pattern_fill.", + "relevance": "entity_create_hatch lines 593 to 606 only calls set_pattern_fill with one add_polyline_path. It cannot produce section view solid fills, gradient fills, curved or island boundaries, or associative hatches.", + "source_url": "https://ezdxf.mozman.at/docs/dxfentities/hatch.html" + }, + { + "title": "Dimension styling via DimStyle plus per dim override dict, and ordinate and arc dims", + "takeaway": "add dim methods return a DimStyleOverride. You pass dimstyle by name and or an override dict with dimtxt dimasz dimclrt dimdec and dimlfac to control text height arrow size color precision and scale. doc dimstyles new creates reusable styles. ezdxf also provides ordinate and arc dimensions and warns it does not honor every dimension variable.", + "relevance": "Every dimension method lines 668 to 774 calls add dim with no dimstyle and no override so all dims use the ezdxf default style with wrong text height and arrow size for ISO sheets. The dimension_auto ordinate branch lines 1988 to 2004 fakes ordinates with two rotated linear dims.", + "source_url": "https://ezdxf.mozman.at/docs/tutorials/linear_dimension.html" + }, + { + "title": "odafc add-on does real DXF to DWG conversion from R12 to R2018", + "takeaway": "ezdxf addons odafc provides is_installed, readfile, export_dwg with doc filename and version, and convert with version defaulting to R2018. It wraps the external ODA File Converter to produce true DWG and to read DWG and DXB, supporting R12 through R2018.", + "relevance": "drawing_save_as accepts the dwg format but just calls doc saveas lines 345 to 352, writing a DXF with a dwg name, silently producing a wrong format file. Wiring odafc export_dwg guarded by is_installed makes DWG export real, and odafc readfile lets drawing_open read dwg files.", + "source_url": "https://ezdxf.mozman.at/docs/addons/odafc.html" + }, + { + "title": "ezdxf query mini language and groupby replace hand rolled filtering", + "takeaway": "Every layout has a query method taking an entity and attribute string that supports negation, and, or, regex, and wildcard exclusion, returning an EntityQuery. groupby with a dxfattrib such as layer or a key function groups entities into a dict in one call.", + "relevance": "analysis_stats hand builds the by type and by layer dicts in a Python loop lines 1293 to 1309, analysis_select methods loop with lower case comparisons lines 1371 to 1390, and entity_select_smart pulls 5000 entities then filters in memory lines 2008 to 2050. The query method plus groupby is faster, supports compound predicates and regex, and cuts code.", + "source_url": "https://ezdxf.readthedocs.io/en/stable/tasks/groupby.html" + }, + { + "title": "MTextEditor for inline MTEXT formatting and recover module for damaged files", + "takeaway": "ezdxf tools text MTextEditor builds inline MTEXT codes for font color height stacked fractions bullet lists and paragraph properties. Separately ezdxf recover readfile loads structurally damaged DXF that the normal readfile rejects, and the Auditor fix method via doc audit repairs recoverable errors in memory.", + "relevance": "entity_create_mtext lines 578 to 591 takes a plain string only with no titleblock grade formatting. drawing_open uses plain ezdxf readfile line 322 with no recover fallback, and drawing_audit lines 448 to 457 reports errors but never calls the auditor fix method, so a repair capability is missing.", + "source_url": "https://ezdxf.readthedocs.io/en/stable/tutorials/mtext.html" + }, + { + "title": "ezdxf release line v1.3 in 2024 to v1.4.4 current", + "takeaway": "ezdxf reached v1.3.4 on 2024 10 15 and is now at v1.4.4. v1.3 added layer support to the PyMuPDF drawing backend. The backend creates documents as R2010 at line 302 but ignores almost all post 1.0 add-on capability.", + "relevance": "Establishes that the rendering, export, and query features above are stable in the targeted version range, so adopting them is low risk.", + "source_url": "https://ezdxf.mozman.at/release-v1-3.html" + } + ], + "opportunities": [ + "Replace the hardcoded matplotlib render in drawing_export_pdf at lines 357 to 379 and view_screenshot at lines 1400 to 1424 with a configurable renderer that accepts a Configuration with background_policy, lineweight_policy ABSOLUTE, lineweight_scaling, min_lineweight, and color_policy, and a backend choice; add native SVGBackend with zero extra deps and optional PyMuPdfBackend for crisp PDF and PNG", + "Add layout Page, Margins, and Settings driven paged export so a render can target a specific paperspace layout and sheet size such as A3 or A4 with a proper titleblock and border instead of raw modelspace extents", + "Make DWG real via ezdxf addons odafc export_dwg guarded by is_installed and let drawing_open fall back to odafc readfile for dwg and dxb; the current dwg format at lines 345 to 352 writes DXF bytes into a dwg file", + "Upgrade entity_create_hatch at lines 593 to 606 with set_solid_fill for section views, set_gradient, multiple and island boundary paths, and edge paths with arcs and splines", + "Introduce a reusable ISO DimStyle created once per drawing with doc dimstyles new and dimtxt dimasz dimdec dimclrt dimlfac, and pass dimstyle and override to all add dim calls at lines 668 to 774; add a real ordinate dimension and arc dimension so the dimension_auto ordinate path at lines 1988 to 2004 stops faking it with linear dims", + "Replace hand rolled querying with the ezdxf query method in analysis_select methods and entity_list and groupby in analysis_stats at lines 1293 to 1390; back entity_select_smart with query plus groupby instead of loading 5000 entities and filtering in Python at lines 2008 to 2050", + "Add a drawing_repair tool using ezdxf recover readfile as a fallback in drawing_open at lines 309 to 331 for damaged DXF, plus call the auditor fix method in drawing_audit at lines 448 to 457 to actually repair not just report", + "Expose MTextEditor in entity_create_mtext at lines 578 to 591 for inline formatting such as stacked fractions for tolerances, bullet and numbered notes, and per span font color and height", + "Generalize entity_offset at lines 868 to 908 which is LINE and CIRCLE only and the LINE plus LINE only trim, extend, fillet, and chamfer at lines 1535 to 1750 using the ezdxf path module with Path offset and from_entity and arc aware intersections to handle LWPOLYLINE and ARC corners" + ], + "confidence": "High on all codebase claims. Every cited line range was read directly from backends ezdxf_backend.py, and a Grep over the backends folder for set_gradient, set_solid_fill, paperspace, add_viewport, odafc, export_dwg, groupby, dimstyles, MTextEditor, add_ordinate, add_arc_dim, qsave, and set_pattern_definition returned zero matches, confirming none are used. WebSearch and WebFetch were available. ezdxf docs feature names and signatures are quoted from official ezdxf mozman at and readthedocs pages. The query and groupby page was covered via WebSearch of the same official docs rather than a full fetch, so verify exact method names against the ezdxf readthedocs groupby page before implementation." + }, + { + "topic": "Drafting-standards conformance (ISO 128 / 129 / 13567 / 7200 / 1101, ASME Y14.5) of AutoCAD MCP Pro's engineering layer", + "summary": "The project encodes the \"structural\" half of ISO drafting standards well but the \"annotation/specification\" half barely at all. ISO 128 lineweights and line types are correctly modeled: plan_spec.py defines the exact 9-value ISO 128-2 lineweight set (0.13-2.00 mm) and critique.py enforces it per-layer; layers.py provides three standard layer sets (mech/pid/iso13567) with conforming lineweights and the CENTER/HIDDEN/PHANTOM/DASHED/DASHDOT linetypes. ISO 13567 is approximated by a single hardcoded \"starter\" layer list (M-GEOMET-E-N style) but the field grammar (agent/element/presentation/status field codes) is not actually parsed or validated. ISO 7200 title blocks exist (titleblock.py) with most mandatory fields, but the layout geometry is hardcoded to A3 only, several text-placement constants look mis-derived, and label terminology is informal (\"DWG NO\" vs ISO field names). ISO 129 dimensioning is the weakest structural area: dimension_auto claims \"ISO 129 dimensions\" but the dimension_* backend signatures (base.py:226-257) carry NO parameter for tolerances, limit/fit deviations, or text override, so symmetric (+/-), limit, and fit-class dimensions per ISO 129-1 simply cannot be authored. ISO 1101 GD&T and ASME Y14.5 are entirely absent: there is no feature control frame, datum, or geometric-tolerance primitive anywhere in the codebase (confirmed by grep across engineering/ and backends/). The validator/critique only sanity-check geometry hygiene (fake-dim text, untrimmed corners, layer color, construction leftovers) and do not validate dimensional completeness or any tolerancing standard. Net: good for un-toleranced ISO 128/7200-style geometry on A3; not yet capable of production-quality drawings that require dimensional tolerances or GD&T.", + "findings": [ + { + "title": "ISO 128-2:2020 line widths (9-value set) and narrow/wide/extra-wide groups", + "takeaway": "ISO 128-2 defines exactly 9 line widths (0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0, 1.4, 2.0 mm) grouped as narrow/wide/extra-wide, plus 15 numbered line patterns (clause 4.2). Width/group is chosen by drawing type, size and scale.", + "relevance": "The project already hardcodes this exact set in engineering/plan_spec.py:85 (ISO_128_LINEWEIGHTS_MM) and enforces it in engineering/critique.py:37-64 (_check_iso128). This is the project's strongest conformance point. Gap: it validates only that a layer's lineweight is IN the set, not that the right group (narrow vs wide) is used for the right element class (e.g. visible edges should be wide ~0.5, dims/center narrow ~0.18) and not the line-pattern numbering.", + "source_url": "https://www.iso.org/standard/83355.html", + "date": "2022" + }, + { + "title": "ISO 129-1: dimensioning elements and tolerance indication", + "takeaway": "ISO 129-1 specifies the dimensioning elements (dimension line, value, arrowhead terminator, extension line, leader, origin symbol, centre line) and tolerance indication: symmetric tolerances use a single value preceded by +/-, and limit dimensions show explicit max and min. Dimension lines are continuous narrow lines per ISO 128-20.", + "relevance": "This is the biggest functional gap. backends/base.py:226-257 dimension_linear/aligned/angular/radius/diameter and base.py:570-578 dimension_auto have NO parameter for tolerance, limit deviations, fit class, or dimension-text override. dimension_auto's docstring claims 'ISO 129 dimensions' but cannot emit +/- symmetric, limit (max/min), or fit (e.g. H7/g6) tolerances at all. A production part drawing is impossible without this.", + "source_url": "https://www.iso.org/obp/ui/#iso:std:iso:129:-1:ed-1:v1:en", + "date": "2004" + }, + { + "title": "ISO 13567-1:2017 layer-name field grammar (agent / element / presentation mandatory; status, sector, phase optional)", + "takeaway": "Layer names are a fixed-length, continuous alphanumeric string of mandatory fields (agent responsible, element, presentation) plus optional fields (status, sector, phase, projection, scale, work package). Short names use only mandatory fields; complex projects use more.", + "relevance": "engineering/layers.py:51-62 provides a single hardcoded ISO13567_LAYERS 'starter' list (M-GEOMET-E-N etc.) but there is no parser/validator for the field grammar, so a user-supplied layer name is never checked for ISO 13567 conformance, and the codes are mechanical-discipline-only. Opportunity: add a field-code builder/validator (agent+element+presentation[+status]) instead of a static list.", + "source_url": "https://www.iso.org/standard/70181.html", + "date": "2017" + }, + { + "title": "ISO 7200:2004 mandatory data fields (8) and 180 mm title-block width", + "takeaway": "ISO 7200 defines 8 mandatory fields (Legal owner, Identification/drawing number, Date of issue, Sheet/segment number, Title, Approval person, Creator, Document type) plus 11 optional (revision index, paper size, document status, classification, responsible dept, etc.). Title block is 180 mm wide and reused across all paper sizes (A4 left margin 20 mm / right 10 mm).", + "relevance": "engineering/titleblock.py models title/drawing_no/sheet/revision/scale/material/part_no/drawn_by/checked_by/date/company and uses the correct 180 mm TB width (TB_WIDTH=180.0) and 20/10 mm margins. Gaps: (1) ISO mandatory fields 'Document type' and explicit 'Date of issue' vs informal 'DATE', and 'Creator/Approval person' labeling differ from ISO field names; (2) geometry is A3-only (SHEET_W/H=420/297 hardcoded) even though plan_spec.SheetSize allows A4-A0; (3) title X-centering at titleblock.py:171 estimates text width as len*0.3*height which will mis-center; (4) several value Y constants (e.g. tb_y0+1.5 vs value_pad_y) appear inconsistent row-to-row.", + "source_url": "https://www.iso.org/standard/35446.html", + "date": "2004" + }, + { + "title": "ISO 1101 vs ASME Y14.5 GD&T: 14 symbols, feature control frames, datum reference frames", + "takeaway": "Both standards share ~14 geometric tolerance symbols and the feature-control-frame + datum-reference-frame (primary A / secondary B / tertiary C) concept. Differences: ASME Y14.5-2018 dropped Concentricity & Symmetry (use Position); ISO keeps them. ISO separates size vs form (size tolerance controls size only; envelope principle must be invoked explicitly), ASME defaults differently and states material conditions explicitly. ISO splits rules across many GPS documents; default tolerances often via ISO 2768.", + "relevance": "Entirely absent from this project: grep across engineering/ and backends/ found no feature-control-frame, datum, or geometric-tolerance primitive. No way to place a FCF, datum target, or even general ISO 2768 tolerance note. For 'production-quality engineering drawings' this is a categorical miss alongside the ISO 129 tolerance gap.", + "source_url": "https://www.gdandtbasics.com/iso-vs-asme-standards/", + "date": "2026" + }, + { + "title": "ISO 128-20 / line conventions for section views and centre lines", + "takeaway": "ISO 128 conventions: cutting planes / section indication use long-dash-dot (phantom-type) lines with wide ends; centre lines are narrow long-dash-dot; hatching is narrow continuous at 45 degrees. Dimension and extension lines are narrow continuous.", + "relevance": "engineering/layers.py maps CENTER->CENTER linetype@0.18, PHANTOM->PHANTOM@0.18, HATCH->Continuous@0.13, which matches ISO line-class intent. engineering/section.py provides cutting_plane_line + apply_section_hatch. Gap to verify in a deeper pass: whether the cutting-plane line uses the wide-end / phantom convention and whether hatch angle/spacing follows ISO 128-50; critique.py does not check section-view line conventions.", + "source_url": "https://en.wikipedia.org/wiki/ISO_128", + "date": "2024" + } + ], + "opportunities": [ + "Add tolerance support to the dimension layer: extend dimension_linear/aligned/radius/diameter and dimension_auto (backends/base.py:226-257, 570-578) with optional params for symmetric (+/-), limit (upper/lower deviation), and ISO 286 fit-class (e.g. 'H7'/'g6') text, so dimension_auto's 'ISO 129' claim becomes real.", + "Implement a feature-control-frame primitive (ISO 1101 / ASME Y14.5): a gd_frame(symbol, tolerance, datums=[A,B,C], modifiers=[M/L/P]) tool plus datum-feature and datum-target symbols, rendered as MTEXT-in-boxes or true AcDbFcf where COM allows. This is the single largest missing capability for production drawings.", + "Add an ISO 2768 / general-tolerance title-block note option (e.g. 'ISO 2768-mK') so un-toleranced dims have a defined default, which ISO 1101 requires when no explicit GD&T is given.", + "Generalize titleblock.py beyond A3: derive SHEET_W/H from plan_spec.SheetSize (A4-A0) instead of hardcoded 420x297 (titleblock.py:16-17), keep the 180 mm block constant per ISO 7200, and reuse one block across sizes.", + "Rename title-block labels to ISO 7200 field names and add the missing mandatory fields (Document type, explicit Date of issue, Creator, Approval person), and replace the len*0.3 title-centering heuristic (titleblock.py:171) with a measured text width or a justify/anchor flag from the backend.", + "Turn ISO13567_LAYERS (layers.py:51-62) from a static mechanical-only list into a field-code builder+validator (agent/element/presentation[+status/sector/phase]) so arbitrary layer names can be checked for ISO 13567 conformance, and add a critique focus 'iso13567_naming'.", + "Extend critique.py with dimensional-completeness and ISO-129-line-class checks: flag geometry features (holes/edges) with no associated dimension, and verify dimension lines/extension lines resolve to a narrow-group lineweight; also add a section-view line-convention check (phantom cutting plane, 45-degree hatch).", + "Tie the ISO 128 lineweight check to element class, not just set membership: validate that visible edges use a wide group weight and dims/center/hatch use narrow, rather than only checking each layer's weight is one of the 9 allowed values (critique.py:37-64)." + ], + "confidence": "High on the codebase assessment: all four target files (engineering/layers.py, titleblock.py, validator.py, critique.py) plus plan_spec.py, __init__.py and the dimension/ISO method signatures in backends/base.py were read directly, and the absence of GD&T/tolerance support was confirmed by grep across engineering/ and backends/. WebSearch was available and used for all six standards; WebFetch was available but not needed (search snippets were sufficient and primary ISO.org pages corroborate). Medium confidence only on section.py line conventions and the exact title-block text-placement math, which were read but not exhaustively traced." + } + ], + "dossier": "The lineweight truncation bug is confirmed devastating: ENGINEERING_LAYERS uses floats like 0.50, 0.25, 0.18, 0.13, 0.05 mm, and `layer_create` does `int(lineweight)` (ezdxf_backend.py:1034), so every one floors to 0. Counts confirmed: 109 tools, 6 resources, 5 prompts; LICENSE/benchmarks/ci.yml all absent. All load-bearing claims verified against source. I have everything needed to synthesize.\n\n# AutoCAD MCP Pro — Understanding & Research Dossier\n\n## 1. Architecture at a Glance\n\nAutoCAD MCP Pro is a **single-file FastMCP 3.0 server** (`server.py`, 2881 lines) exposing **109 `@mcp.tool` registrations, 6 `@mcp.resource` endpoints, 5 `@mcp.prompt` templates** (verified by grep, not the docs' \"87\"). It uses a **strategy-pattern dual backend** behind one ABC (`backends/base.py`, ~73 abstract methods + 5 dataclasses): a **live COM backend** (`com_backend.py`, pywin32, single STA-thread `ThreadPoolExecutor`) and a **headless ezdxf backend** (`ezdxf_backend.py`, file-based DXF, all sync work via `asyncio.to_thread` under one `asyncio.Lock`). Backend selection (`server.py::_make_backend`, server.py:69): env `AUTOCAD_MCP_BACKEND`; Windows `auto`/`com` tries COM then falls back to ezdxf; non-Windows always ezdxf.\n\nRequest path: `ErrorHandlingMiddleware → AuditMiddleware → TimingMiddleware → LoggingMiddleware` (server.py:197-200) → tool fn → `_backend(ctx)` (server.py:207, raises `ToolError` if backend None) → identically-named backend method → `_dc()` dataclass→dict serialization (server.py:216). Lifespan (`autocad_lifespan`, server.py:107) builds the backend singleton into `ctx.lifespan_context[\"backend\"]`.\n\nA **deterministic engineering layer** (`engineering/` package) backs the \"premium\" workflow: parametric gear/keyway/titleblock primitives, a closed-enum `drawing_critique`, an 8-step `DrawingValidator`, ISO layer/linetype scaffolds, and a `PlanSpec` plan-before-draw contract. **The central architectural fact: the premium workflow is fully implemented only on ezdxf; the COM backend stubs all 8 premium meta-tools with `NotImplementedError` (com_backend.py:1637-1660, verified) — yet COM is the default Windows backend.**\n\n---\n\n## 2. Per-Subsystem Summary\n\n**`server.py` (transport + tool registry).** Thin async wrappers over the backend singleton, grouped into 13 sections (1–12 + sub-sections 8b/8c/8d batch/template/validation + 13 premium). Owns server-side composition tools (`entity_delete_many` server.py:1065, `layer_isolate` :1216, `block_find_references` :1362, `analysis_layer_stats` :1506, `entity_batch_*` :1538/:1590, `template_*` :1695/:1730, `validation_check` :1749). Entry point manually parses `--transport/--port/--host` (server.py:2866) and refuses non-loopback HTTP unless `ALLOW_REMOTE_HTTP` + `MCP_AUTH_TOKEN` (`_validate_http_bind`).\n\n**`backends/base.py` (the contract).** `AutoCADBackend` ABC: ~73 `@abstractmethod`, 2 concrete (`set_layer_active`, `ensure_linetypes`), 5 dataclasses (`EntityInfo/LayerInfo/BlockInfo/DrawingInfo/CommandResult`), 3 helpers (`shoelace_area`, `deg2rad`, `rad2deg`). Both backends define a body for every abstract method (no structural gaps); divergences are behavioral.\n\n**`backends/ezdxf_backend.py` (headless).** Full create/modify/query/analysis/dimension/block/layer + premium meta-tools + corner ops. Transactions = full DXF snapshot to tempfile on `_undo_stack` (trimmed to `max_undo_stack=5`, O(drawing-size) per `transaction_begin`). View ops and `system_run_command/run_lisp` are documented no-ops. Screenshot/PDF via matplotlib.\n\n**`backends/com_backend.py` (live AutoCAD).** All COM through one STA worker with per-call timeout (`com_call_timeout`, default 60s) and executor-rebuild on timeout/fatal HRESULT. Corner ops via `_safe_send_command` (CMDACTIVE-polling). Win32 `PrintWindow` screenshots. **8 premium meta-tools + `block_create_from_entities` are stubs.**\n\n**`engineering/` (deterministic CAD).** `gear.py` (involute spur/helical outlines), `keyway.py` (DIN 6885), `titleblock.py` (ISO 7200 A3), `layers.py` (mech/pid/iso13567 sets), `critique.py` (6 focuses), `validator.py` (8-step gate), `plan_spec.py` (PlanSpec/Issue + ISO-128 lineweight set). `section.py` (`cutting_plane_line`, `apply_section_hatch`) and `gear.generate_tooth_profile` are **dead code** (exported, zero callers/tests).\n\n**`tests/` (184 functions, 14 files).** One ezdxf-only `backend` fixture (`conftest.py`). COM backend, server→sanitizer wiring, and `section.py` have **zero coverage**.\n\n---\n\n## 3. Complete MCP Tool Inventory (grouped; ✅ real / ⚠️ partial / ❌ stub-or-missing)\n\n> All 109 tools are **registered** in `server.py` and have **real ezdxf implementations**. \"Status\" below reflects the **COM backend** (the Windows default) plus correctness caveats. Nothing is purely aspirational/unregistered.\n\n| § | Tools | ezdxf | COM | Notes |\n|---|---|---|---|---|\n| 1 Drawing (12, doc says 11) | info, new, open, save, save_as, export_dxf, export_pdf, purge, audit, close, undo, redo | ✅ (redo ❌ no-op ezdxf_backend.py:487) | ✅ | `drawing_redo` inert on ezdxf; header says 11 but 12 registered (drawing_redo server.py:426) |\n| 2 Entity Create (13) | line, circle, arc, polyline, rectangle(→polyline), text, mtext, hatch, spline, ellipse, point, block_ref | ✅ | ✅ | mtext has no rotation param on ezdxf (:578); hatch pattern-only single loop |\n| 3 Dimensions (5) | linear, aligned, angular, radius, diameter | ⚠️ aligned/angular **xfail** (tests/test_dimensions.py:16); angular ignores tx/ty + fixed dist=10 (ezdxf:705) | ✅ | No dimstyle/tolerance support either backend |\n| 4 Modify (14) | move, copy, rotate, scale, mirror, offset, trim, extend, fillet, chamfer, delete, array_rectangular, array_polar, set_properties | ⚠️ offset ignores side (:868); corner ops LINE+LINE only (:1535-1750); polar 360° dup (:946) | ⚠️ offset ignores side (:860); corner ops via SendCommand | |\n| 5 Query (3) | entity_get, entity_list, entity_delete_many | ✅ | ✅ | COM entity_list = O(n) per-item COM reads (:967) |\n| 6 Layer (14) | list, create, delete, set_current, modify, freeze, thaw, lock, unlock, hide, show, isolate, linetype_list, linetype_load | ⚠️ **lineweight int() truncation** (:1034) | ✅ | See RISK-2 |\n| 7 Block (7) | list, insert, explode, get_attributes, set_attributes, create_from_entities, find_references | ✅ | ⚠️ create_from_entities ❌ stub (:1253) | BlockInfo.description never populated |\n| 8 Analysis (8) | stats, find_in_region, measure_distance, measure_area, bounding_box, select_by_layer, select_by_type, layer_stats | ⚠️ region fallback insert/start only (:1330) | ✅ | hand-rolled loops vs ezdxf query/groupby |\n| 8b Batch (2) | entity_batch_create, entity_batch_modify | ✅ | ✅ | |\n| 8c Template (2) | template_apply_layers, template_list | ✅ | ✅ | Duplicates `_LAYER_TEMPLATES`/prompt layer sets |\n| 8d Validation (1) | validation_check | ✅ | ✅ | |\n| 9 View (4, doc says 5) | zoom_extents, zoom_window, screenshot, zoom_and_screenshot | ⚠️ zoom = no-op (:1394) | ✅ | Header says 5 |\n| 10 Transactions (3) | begin, commit, rollback | ✅ (commit file-I/O outside lock :1452) | ⚠️ flag-leak on error (:1402) | |\n| 11 System (6) | status, get_variable, set_variable, run_command, run_lisp, about | ⚠️ run_command/lisp no-op | ⚠️ run_lisp result always 'nil' (:1487); about catalog drifted | |\n| 12 Engineering (7) | gear_draw_helical/spur/section_aa, keyway_draw_keyed_bore/section, titleblock_apply_iso_a3, drawing_finalize | ✅ | ✅ (use generic entity_create_*) | Validator/titleblock import-guarded → None risk |\n| 13 Premium (8) | drawing_plan, drawing_critique, point_from_snap, construction_xline, construction_clear, drawing_apply_iso_layers, dimension_auto, entity_select_smart | ✅ | ❌ **all NotImplementedError** (:1637-1660) | **The documented-mandatory workflow is dead on COM** |\n\n**Documented-but-missing entirely:** none. **Documented-as-working but stubbed on default backend:** the 8 premium tools (§13) + `block_create_from_entities` on COM.\n\n---\n\n## 4. Cross-Cutting RISK Register (merged, deduped, severity-sorted)\n\n| # | Sev | Risk | Location | Evidence |\n|---|---|---|---|---|\n| R1 | **CRITICAL** | All 8 premium meta-tools `raise NotImplementedError` on COM (the Windows default). The \"non-negotiable for production\" workflow (`drawing_plan→apply_iso_layers→critique→finalize`) 500s on the primary platform. | com_backend.py:1637-1660 | **Verified verbatim**; ezdxf impls real (ezdxf_backend.py:1753-2050) |\n| R2 | **CRITICAL** | `layer_create` does `int(lineweight)` so every sub-1.0 ISO mm value (0.50/0.25/0.18/0.13/0.05) floors to **0**. The entire ISO-128 lineweight discipline is silently nulled; `drawing_apply_iso_layers`/`drawing_new` bootstrap produce flat 0-weight layers. | ezdxf_backend.py:1034; values engineering/layers.py:16-26 | **Both verified.** ezdxf wants integer hundredths (50=0.50mm), not raw mm |\n| R3 | **HIGH** | `_check_iso128` is a permanent no-op: `lw <= 0: continue` (critique.py:50) skips every layer made 0 by R2. The advertised ISO gate can never fire. Comment \"the layer dataclass already normalises to mm\" (critique.py:53) is **false** (ezdxf_backend.py:189 stores raw `float(lw)`). | engineering/critique.py:37-64 | **Verified verbatim** |\n| R4 | **HIGH** | `_check_dim_overlap` reads `properties['text_position'/'defpoint'/'insertion_point']`, but the ezdxf DIM extractor sets only `dim_type` (ezdxf_backend.py:166-168) → `dim_points` always empty → check is a silent no-op. | engineering/critique.py:194-197 | — |\n| R5 | **HIGH** | COM timeout path (`stuck.shutdown(wait=False)`) cannot kill the STA worker still blocked in SendCommand → **permanently leaks one CoInitialize'd thread + COM proxy per timeout**; no `CoUninitialize` anywhere. | com_backend.py:415-433, 68-71 | — |\n| R6 | **HIGH** | Zero COM-backend test coverage (~80 methods incl. UndoMark transactions, SendCommand corner ops, CMDACTIVE guard). Passing ezdxf transaction tests give **false confidence** for the semantically different COM model. | tests/ (no ComBackend import); conftest.py:8-15 | — |\n| R7 | **HIGH** | Server→sanitizer wiring untested: `test_security.py` tests `sanitize_command/lisp/validate_path` in isolation; no integration test proves `system_run_command/lisp` actually call them. A refactor dropping the call (server.py:2035/2052) passes all security tests while raw commands reach SendCommand. | server.py:2035,2052; tests/test_security.py | — |\n| R8 | **HIGH** | LISP-aliasing bypass (`(setq f command)(f \"ERASE\")`) is claimed-defended in a comment (security.py:138-140) but has **no adversarial test**; UNC/`~`/symlink path escapes in `validate_path` also untested. | security.py:138-179; test_security.py:177-231 | — |\n| R9 | **MED** | `entity_array_polar` full-360° fill double-places: `step=radians(360)/(count-1)` puts the last copy at 0°==original → duplicate. Hits the common bolt-circle/gear pattern the engineering layer relies on. | ezdxf_backend.py:946-947 | — |\n| R10 | **MED** | `drawing_finalize` calls `drawing_save_as(str(validated))` with no `fmt`. ezdxf default `fmt='dxf'` (ezdxf:345) vs COM `fmt='dwg'` (com:515) vs save_as tool default `'dwg'` (server:336) → 3-way inconsistency; format backend-dependent, not path-derived. | server.py:2299 | **Verified** (omits fmt) |\n| R11 | **MED** | `drawing_save_as(fmt='dwg')` on ezdxf writes DXF bytes to a `.dwg` path and returns `{'format':'dwg'}` — silent wrong-format file. ezdxf cannot write DWG; should error or use `odafc`. | ezdxf_backend.py:345-352 | — |\n| R12 | **MED** | `dimension_angular` ignores tx/ty, fixed `distance=10.0` → arc radius wrong for large/small parts; breaks COM parity. | ezdxf_backend.py:705-722 | — |\n| R13 | **MED** | `entity_offset` ignores `side_x/side_y` (both backends); only `distance` sign flips side; ARC/LWPOLYLINE/ELLIPSE raise \"not supported\". | ezdxf:868-908; com:860-874 | — |\n| R14 | **MED** | Engineering titleblock/validator symbols import-guarded to `None` (engineering/__init__.py:40-51); if module truly fails to import, tools raise opaque \"NoneType not callable\" instead of clear ToolError. | server.py:2266,2294 | — |\n| R15 | **MED** | `system_about` static `tool_groups` catalog drifted: omits all 15 engineering+premium tools, misfiles `entity_delete_many`. Agents inspecting capabilities can't see the CLAUDE.md-mandated tools. | server.py:2070-2106 | — |\n| R16 | **MED** | COM `transaction_active` left stale-True if commit/rollback `_run` raises; `rollback` issues `EndUndoMark`+`UNDO B` SendCommand without the CMDACTIVE guard used elsewhere → can interleave mid-command. | com_backend.py:1402,1411-1421 | — |\n| R17 | **MED** | `_safe_send_command` busy-polls CMDACTIVE with `time.sleep(0.05)` on the single STA thread → monopolizes the executor up to deadline; `deadline_s`(8s) decoupled from `_run` timeout(60s). | com_backend.py:1521-1536 | — |\n| R18 | **MED** | `drawing_new` swallows bootstrap failures into `result['bootstrap']={'error':...}` and reports success → un-bootstrapped drawing looks normal though CLAUDE.md treats the scaffold as a precondition for gear_*/titleblock_*. | server.py:280-294 | — |\n| R19 | **MED** | `dimension_aligned`/`dimension_angular` permanently `xfail(strict=False)` — advertised tools shipped broken; an accidental fix wouldn't alert. | tests/test_dimensions.py:16-27 | — |\n| R20 | LOW | `_registered_tool_count` reaches into FastMCP privates (`mcp._local_provider._components`, `'tool:'` prefix); a minor upgrade → tool_count=-1 surfaced to clients. | server.py:231-234 | — |\n| R21 | LOW | Involute flank prepends a straight radial root→base segment (no trochoid fillet) → geometrically wrong dedendum for `root_r=base_r`; degenerate arc for teeth≤2. | engineering/gear.py:40-50,163-190 | — |\n| R22 | LOW | `drawing_undo`/`transaction_rollback` share one `_undo_stack` with no transaction-id/depth separation; reload doesn't reset `_doc_path`/`_current_layer`. | ezdxf_backend.py:470-484,1462-1476 | — |\n| R23 | LOW | COM `system_run_lisp` always reports `result='nil'` (SendCommand returns None); no CMDACTIVE guard → a prompting LISP form deadlocks next call. | com_backend.py:1487-1492 | — |\n| R24 | LOW | `_apply_attrs` sets `entity.dxf.linetype` without `_ensure_linetype_loaded` (unlike set_properties/layer_modify) → `linetype='HIDDEN'` on create may render Continuous or raise on save. | ezdxf_backend.py:491-501 | — |\n| R25 | LOW | `block_find_references` calls `entity_list(type_filter='INSERT')` with no limit (bypasses `max_list_limit`) → unbounded INSERTs into memory; assumes every backend populates `properties['block_name']`. | server.py:1362-1370 | — |\n| R26 | LOW | View zoom ops unconditionally registered but no-op on ezdxf → success response implies the view changed when it didn't; `view_zoom_and_screenshot` may not frame the requested region on ezdxf. | server.py:1814-1831,1870 | — |\n| R27 | LOW | `validator.py` title check substring-matches 'SPUR'/'HELICAL SPUR' on TEXT layer → false-positive blocks `drawing_finalize` if any note contains 'SPUR'. | engineering/validator.py:166-181 | — |\n| R28 | LOW | COM `_find_autocad_hwnd` grabs first visible window whose title contains 'AutoCAD' → may capture palette/splash/About dialog; not tied to `AcadApplication.HWND`. | com_backend.py:283-300 | — |\n| R29 | LOW | Broad `except Exception: pass` swallows COM faults (disconnected/busy) at debug level in ~20 sites → bypasses the HRESULT reconnect logic in `_run`. | com_backend.py:199,244,868,901,1216,… | — |\n\n---\n\n## 5. Doc-vs-Code Drift Table\n\n| Claim (source) | Reality | Verdict |\n|---|---|---|\n| \"87 tools\" (CLAUDE.md, README:6) | **109** `@mcp.tool` (verified) | STALE (soft — both add \"count is dynamic\" caveat) |\n| \"104 tests\" (README:83) | **184** test functions across 14 files (verified) | STALE (~80 under) |\n| Section 1 \"(11 tools)\" (CLAUDE.md) | 12 (`drawing_redo` server.py:426) | WRONG |\n| Section 9 \"(5 tools)\" (CLAUDE.md) | 4 view tools registered | WRONG |\n| \"12-section taxonomy\" (CLAUDE.md) | 1–12 + 8b/8c/8d + 13 = 6 extra groups | INCOMPLETE |\n| Premium workflow \"non-negotiable for production\" (CLAUDE.md) | 8/8 premium tools dead on COM (default Windows backend) | OVERCLAIM |\n| \"MIT — see [LICENSE](LICENSE)\" (README:409) | No LICENSE file (verified absent) | **DEAD LINK / legal drift** |\n| `benchmarks/` referenced (README:39,302,367) | Absent (verified) | DEAD LINK (forward-looking) |\n| \".github/workflows/ci.yml\" + \"GitHub Actions CI\" (README:84,358) | git status `D`; absent on disk (verified) | **CI REMOVED while docs claim it** |\n| \"Premium discipline lives in `.claude/skills/autocad-mcp-premium/`\" (CLAUDE.md) | Entire tree git-deleted (28 deletions) + all `.cursor/` deleted | DANGLING POINTER |\n| README tests/ tree (README:347-355) | Omits 6 of 14 test files + engineering/ package | STALE |\n| `system_about` tool_groups | Omits 15 engineering+premium tools | DRIFT |\n| `BlockInfo.description` field (base.py:71) | Never populated by either backend | DEAD FIELD |\n| critique.py:53 \"normalises to mm\" comment | Stores raw DXF lineweight (ezdxf:189) | FALSE COMMENT |\n| `engineering/__init__.py:37-39` \"Agent B … in flight\"; com_backend \"Task #7/#8/#9\" | Multi-agent build seams shipped unreconciled | TODO-IN-PROD |\n| Dockerfile, .env.example (README) | Present (verified) | OK |\n\n---\n\n## 6. Test Coverage Gaps (ranked)\n\n1. **COM backend: 0% coverage** (com_backend.py entire file) — transactions (UndoMark vs ezdxf snapshot are *different* semantics), SendCommand corner ops, CMDACTIVE guard, timeout/HRESULT recovery, all 8 premium stubs. Highest-risk untested surface; it's the production path. (R6)\n2. **Server→sanitizer integration** — no test proves tools invoke `sanitize_command/lisp`/`validate_path`. (R7)\n3. **Sanitizer adversarial cases** — LISP aliasing, command word-boundary evasion (`.ERASE`), UNC/`~`/symlink path escapes — the comment claims defenses with no tests. (R8)\n4. **Critique detection paths** — `iso128`, `layer_color`, `dim_overlap` only exercised on a *clean* drawing (assert they *don't* fire); their actual detection logic and the unknown-focus branch (critique.py:241) are never proven. (R3, R4)\n5. **`dimension_auto` ordinate** (ezdxf:1988) untested; chain/baseline covered.\n6. **`dimension_aligned`/`angular`** permanently xfail — shipped broken. (R19)\n7. **Uncovered backend methods (both backends):** `system_get/set_variable`, `system_status`, `drawing_close`, `drawing_export_pdf`, `entity_create_block_ref`, `block_set_attributes`, `linetype_list/load` (load-bearing for bootstrap), `view_zoom_*`, `view_screenshot`.\n8. **`engineering/section.py`** (dead code) — 0 coverage.\n9. **Transaction nesting/error semantics** — only single begin→commit/rollback tested; nested, rollback-without-begin, layer/block restoration untested.\n10. **Infrastructure:** CI workflow deleted (no PR gate); `pytest-cov` declared but no `--cov`/`fail-under`; several \"template/validation\" tests assert nothing about the named feature (vacuous `if start:` guards, test_batch_template.py:30-34).\n\n---\n\n## 7. Research Synthesis\n\n### 7a. Academic techniques (2025–2026) that validate or upgrade this design\n\n- **GeoGramBench (ICLR 2026, arxiv.org/abs/2505.17653):** frontier LLMs score **<50%** at high-abstraction Program-to-Geometry. → Empirically justifies the repo's no-coordinate-guessing rule and `point_from_snap`/`entity_select_smart`. **Action:** expand the deterministic geometry surface (`point_intersection`, `point_offset`, `point_along`) so the LLM *never* computes coordinates.\n- **Self-Improving CAD w/ FEA feedback (arxiv.org/html/2605.17448v2):** deterministic controller owns validation, LLM owns design; **+13.4pp** per feedback round; frontier models get **zero** first-attempt strict passes — *iteration beats more tokens*. → This is the repo's architecture, but `drawing_critique` is one-shot. **Action:** make it a routed repair loop; `Issue` (plan_spec.py:64-81) already carries severity/handles/detail — add per-finding repair hints.\n- **CADSmith (arxiv.org/abs/2603.26512):** 5-agent Planner/Coder/Executor/Validator/**Refiner**, dual loop, exact-kernel measurements + **independent VLM judge** → Chamfer 28.37mm→0.74mm (**38×**), IoU 0.81→0.96; *removing the rendered image alone made hard cases 35× worse.* → The repo renders a screenshot at finalize (server.py:2301-2309) but only checks bytes exist (validator step 7). **Highest-leverage single upgrade: feed that render to a VLM judge + add an automated Refiner.**\n- **CADFusion (ICML 2025, arxiv.org/abs/2501.19054)** & **Seek-CAD (arxiv.org/abs/2505.17702):** render is a first-class signal; step-wise visual feedback catches errors mid-build. → Add a `visual` critique focus; run critique per construction step using the existing transaction model.\n- **ProCAD \"Clarify Before You Draw\" (arxiv.org/html/2602.03045v1):** one-round clarification → invalidity 14.6%→0.9%, Chamfer ~80% lower. → `drawing_plan` records intent passively; add a pre-plan ambiguity/conflict detector that returns targeted questions before any `entity_create_*`.\n- **FutureCAD (arxiv.org/abs/2603.11831):** NL → exact B-Rep primitive grounding fixes the fillet/chamfer \"which primitive\" problem. → Directly targets the repo's hardest API: trim/extend/fillet/chamfer need exact handles **plus** keep_x/keep_y. NL grounding (\"fillet the top-left corner\") removes the burden GeoGramBench shows LLMs fail at.\n- **Text-to-CadQuery (2505.06507), CAD-Coder (2505.14646), EvoCAD (2510.11631):** field converged on Python/CadQuery macros; open local VLMs read CAD images reliably; candidate-generate + critique-as-fitness raises first-try quality. → Add more parametric meta-tools (the gear/keyway pattern) and/or a sandboxed parametric-script tool; an open VLM (CAD-Coder, **Florence-2 for GD&T**, arxiv.org/abs/2411.03707) keeps the visual tier off frontier APIs.\n- **MUSE (arxiv.org/html/2605.28579) + CadBench (2605.10873):** grade manufacturability/functionality, not just shape; standard metrics = volumetric/surface IoU, Chamfer, **Invalidity Ratio**. → Replace `validator.py`'s regex/title heuristics with a staged code→geometric→rubric-judge protocol; emit a single scalar \"drawing score\" for regression tests (the repo has *no* quantitative quality metric).\n\n### 7b. Competitive landscape\n\n| Product | 3D | Render-critique loop | Validation gate | HTTP auth | Code escape hatch |\n|---|---|---|---|---|---|\n| **AutoCAD MCP Pro** | ❌ 2D only | ⚠️ finalize-only, symbolic | ✅ **unique** (critique+validator+ISO) | ❌ none | ⚠️ COM-only run_command/lisp |\n| build123d-mcp/agentcad (github.com/pzfreo/build123d-mcp) | ✅ | ✅ in-loop | ❌ | — | ✅ |\n| FreeCAD Robust MCP (150+ tools) | ✅ + FEM | ⚠️ | ❌ | — | ✅ |\n| Blender MCP (~22k★) | ✅ | ✅ | ❌ | — | ✅ Python exec |\n| RhinoMCP | ✅ | ✅ viewport | ❌ | — | ✅ |\n| Fusion360/Onshape MCP | ✅ parametric | — | ❌ | ✅ OAuth/keys | — |\n\nSnyk's 9-CAD-MCP roundup (snyk.io/articles/9-mcp-servers-for-computer-aided-drafting-cad-with-ai/): **vision only in FreeCAD/Blender/Rhino; design validation in none; HTTP auth only in Onshape/Fusion.** → **Our moat is the design-rule validation gate; the field beat us on 3D, in-loop render-critique, and auth.** Autodesk's own in-AutoCAD assistant demo (aecmag.com/features/autodesk-shows-its-ai-hand/) flagship = **automated compliance checking** — exactly our niche, so we must make standards compliance the sharpest, most-cited capability. Adam/Zoo (techcrunch.com/2025/10/31/...) set the UX bar: **parametric editability + post-gen dimension sliders + entity-level conversational editing**.\n\n### 7c. Under-used COM / ezdxf API + ISO standards\n\n**COM (ActiveX) gaps (every create call hits ModelSpace, com_backend.py:125-127):**\n- **PaperSpace/Layouts + Plot config** — `drawing_export_pdf` (com_backend.py:528-534) calls bare `Plot.PlotToFile(path,'DWG To PDF.pc3')` with no page setup → ignores the A3/scale `drawing_plan` promised. Add `layout_*`/`viewport_*` (AddPViewport, StandardScale, CanonicalMediaName). (help.autodesk.com ActiveX ref)\n- **AddTable** (BOM/revision tables), **AddMLeader** (ISO leader notes — CLAUDE.md *reserves* \"leader notes\" but no tool creates them), **Fields** (auto-updating title block), **GetDynamicBlockProperties** (parametric symbol libraries — `block_insert` only does attribute text, com:1195-1251), **DimStyles.Add+CopyFrom** (no ISO-129 dimstyle is ever set → undercuts the \"Dimensions are ISO 129\" rule), **named UCS** (per-view frames for `view_count`), **AttachExternalReference** (shared borders), **gradient/island hatch**.\n- **3D solids/regions/booleans** — `system_status` advertises `all_entity_types` (com:1443) which is **false**; either implement `solid_*` or correct the claim.\n- **Push selection into AutoCAD:** `entity_list` (com:967) and `analysis_select_by_type` (com:1352) loop ModelSpace.Item(i) in Python (O(n) COM round-trips — the #1 perf killer); `analysis_select_by_layer` (com:1339) already shows the right `SelectionSet.SelectAll` group-code-filter pattern to copy.\n\n**ezdxf gaps (Grep confirms zero use of `set_gradient/set_solid_fill/paperspace/add_viewport/odafc/export_dwg/groupby/dimstyles/MTextEditor/add_ordinate/qsave`):**\n- **`addons.odafc.export_dwg`** = real DWG (R12–R2018) → fixes R11/R10 (drawing_save_as fake DWG). (ezdxf.mozman.at/docs/addons/odafc.html)\n- **drawing-addon `Configuration`** (lineweight_policy ABSOLUTE, background_policy, SVGBackend/PyMuPdfBackend) → true ISO lineweights + vector output, replacing raw matplotlib (ezdxf:357-379,1400-1424).\n- **`query()` mini-language + `groupby()`** → replace hand-rolled loops in analysis_stats/select/entity_select_smart (which pulls 5000 entities then filters in memory, ezdxf:2008-2050).\n- **`recover.readfile` + `Auditor.fix`** → real repair; `drawing_audit` (ezdxf:448) reports but never fixes; `drawing_open` (ezdxf:309) has no recover fallback.\n- **`MTextEditor`** (stacked tolerance fractions), **Hatch gradients/islands/edge-paths**, **DimStyle overrides** (no ISO dimstyle today, ezdxf:668-774), **path-module offset** (generalizes LINE/CIRCLE-only offset + LINE-only corner ops).\n\n**ISO standards conformance:**\n- ✅ **ISO 128-2** lineweight set encoded (plan_spec.py:85, verified) — but **nulled by R2** and group-vs-element-class not checked.\n- ⚠️ **ISO 7200** title block — A3-only hardcoded (titleblock.py:16-17), informal labels (\"DWG NO\" vs ISO field names), len×0.3 centering heuristic (titleblock.py:171), missing \"Document type\"/\"Date of issue\".\n- ❌ **ISO 129-1 dimensioning** — biggest functional gap: `dimension_*` (base.py:226-257) have **no** param for ±/limit/fit (H7/g6) tolerances or text override → toleranced production dims impossible. (iso.org/obp/ui/#iso:std:iso:129)\n- ❌ **ISO 1101 / ASME Y14.5 GD&T** — entirely absent (grep-confirmed): no feature control frame, datum, or ISO 2768 general-tolerance note. (gdandtbasics.com/iso-vs-asme-standards/)\n- ⚠️ **ISO 13567** — static mech-only starter list (layers.py:51-62), no field-grammar parser/validator.\n\n---\n\n## 8. \"What Would Make This the BEST AutoCAD MCP\" — Prioritized Thesis\n\nThe evidence converges: **the repo is architecturally ahead on dual-backend + standards-validation but behind on the closed-loop, the production-output plumbing, and the very workflow it markets.** Priorities, evidence-backed:\n\n**P0 — Make the marketed product actually work on its default platform.**\n1. **Implement the 8 premium meta-tools on COM** (R1, com_backend.py:1637-1660). Today the CLAUDE.md-mandatory workflow 500s on every Windows install. Nothing else matters until this ships.\n2. **Fix the lineweight `int()` truncation** (R2, ezdxf_backend.py:1034 → multiply mm by 100 / map to ACAD enum). This single bug nullifies the ISO-128 system *and* makes the iso128 critique a permanent no-op (R3). The standards-validation gate is the **competitive moat** (Snyk roundup; Autodesk's own flagship demo) — it must be real.\n3. **Add CI + a coverage gate + the first COM smoke tests** (R6, ci.yml deleted; pytest-cov unconfigured). Restore the LICENSE file (legal drift).\n\n**P1 — Own the moat by closing the loop (the #1 accuracy lever in every 2025–26 paper).**\n4. **Turn `drawing_critique` into a routed iterative Refiner loop** with per-`Issue` repair hints and a bounded budget (CADSmith Refiner; FEA-agent +13.4pp/round; Physics-in-the-Loop 3.3–6.0 iters converge). The `Issue` dataclass already carries handles+detail.\n5. **Promote the finalize screenshot to a first-class `visual` critique focus fed to a VLM judge** (CADSmith: dropping the render → 35× worse; CADFusion). Use an **open local VLM** (CAD-Coder / Florence-2 for GD&T) behind a `[vision]` extra so the headless path is unaffected. Add a **dimension round-trip check** (VLM-read dims vs created `dimension_*` entities) to catch what proximity-only `dim_overlap` (R4) cannot.\n6. **Add a ProCAD-style pre-plan clarification pass to `drawing_plan`** (invalidity 14.6%→0.9%): detect missing/conflicting dims, material, tolerance, view_count-vs-intent before any geometry.\n7. **Emit a scalar \"drawing score\" / Invalidity-Ratio** (MUSE/CadBench) so the premium workflow is regression-testable — the repo has no quantitative accuracy metric.\n\n**P2 — Reach production-drawing parity (what un-toleranced ISO 128 can't deliver).**\n8. **ISO 129 tolerances + ISO 1101/Y14.5 GD&T**: add ±/limit/fit params to `dimension_*` and a `gd_frame(symbol, tol, datums, modifiers)` + datum/ISO-2768-note primitive. This is the categorical gap blocking real part drawings; no surveyed text-to-CAD product covers 2D GD&T — a differentiator.\n9. **Real plotted sheets**: COM Layout/PaperSpace/Plot-config so `drawing_plan(sheet_size, scale)` and `titleblock_apply_iso_a3` produce a correct A-series plot; on ezdxf wire `odafc.export_dwg` (kills the fake-DWG R11) and the drawing-addon `Configuration` for true-lineweight SVG/PDF.\n10. **An ISO-129 `DimStyle`** created once and applied to all dims (COM `DimStyles.Add+CopyFrom`; ezdxf dimstyle overrides) so \"Dimensions are ISO 129\" becomes true, and **MLEADER** for the leader notes the docs already reserve.\n\n**P3 — Catch the field on breadth (table-stakes elsewhere, absent here).**\n11. **A 3D solids tier** (extrude/revolve from 2D profiles + booleans on COM; STEP/STL on ezdxf) — *or* correct the false `all_entity_types` claim (com:1443). Every competitor has 3D.\n12. **Parametric editability + entity-level conversational editing** (Adam/Zoo): capture driving dims in PlanSpec, expose a re-drive tool; \"select this circle → make it the 20mm bore\" via existing handle model + `entity_select_smart`/`point_from_snap`.\n13. **Authenticated remote HTTP** (API-key middleware) given `dangerous_commands_enabled` exists (server.py:111); a **standard-parts/symbol library** (`library_insert` over `block_insert`, dynamic-block props); **NL→B-Rep grounding** (FutureCAD) over the brittle trim/fillet/chamfer keep-point API.\n\n**P4 — Hygiene that compounds with the above.** Generalize LINE-only corner ops + LINE/CIRCLE-only offset via the ezdxf path module; replace hand-rolled selection with `query()`/`groupby()` (ezdxf) and `SelectionSet.SelectAll` filters (COM); fix `entity_array_polar` 360° dup (R9), `dimension_angular` tx/ty (R12), the COM thread-leak `CoUninitialize` (R5); delete dead `section.py`/`generate_tooth_profile`; reconcile `system_about` catalog (R15) and the stale README/CLAUDE.md drift (§5).\n\n**Files central to every priority:** `backends/com_backend.py:1637-1660` (P0.1), `backends/ezdxf_backend.py:1034` + `engineering/layers.py:14-26` (P0.2), `engineering/critique.py:37-64,194-197` (P0.2/P1.5), `engineering/plan_spec.py:64-87` (P1.4/P1.6/P2.8), `backends/base.py:226-257` (P2.8), `server.py:528-534/2294-2309` (P2.9)." +} \ No newline at end of file diff --git a/engineering/__init__.py b/engineering/__init__.py index abdf0a2..0447330 100644 --- a/engineering/__init__.py +++ b/engineering/__init__.py @@ -6,7 +6,6 @@ from .gear import ( draw_spur_gear_front_view, generate_full_gear_outline, generate_involute_flank, - generate_tooth_profile, involute_xy, ) from .keyway import ( @@ -32,23 +31,8 @@ from .plan_spec import ( SheetSize, SnapType, ) -from .section import apply_section_hatch, cutting_plane_line - -# Agent B contributes titleblock + validator. Imports are guarded so this -# package remains usable while B is still in flight; once those modules land -# the symbols flow through transparently. -try: - from .titleblock import TitleBlockMetadata, apply_iso_a3_titleblock -except ImportError: - TitleBlockMetadata = None # type: ignore[assignment] - apply_iso_a3_titleblock = None # type: ignore[assignment] - -try: - from .validator import DrawingValidator, ValidationFinding, ValidationResult -except ImportError: - DrawingValidator = None # type: ignore[assignment] - ValidationFinding = None # type: ignore[assignment] - ValidationResult = None # type: ignore[assignment] +from .titleblock import TitleBlockMetadata, apply_iso_a3_titleblock +from .validator import DrawingValidator, ValidationFinding, ValidationResult __all__ = [ "draw_helical_gear_front_view", @@ -56,14 +40,11 @@ __all__ = [ "draw_gear_section_aa", "generate_full_gear_outline", "generate_involute_flank", - "generate_tooth_profile", "involute_xy", "draw_keyed_bore", "draw_keyway_section", "keyway_dimensions", "DIN6885_TABLE", - "cutting_plane_line", - "apply_section_hatch", "ENGINEERING_LAYERS", "STANDARD_LINETYPES", "ensure_engineering_layers", diff --git a/engineering/critique.py b/engineering/critique.py index 94c1200..06227a3 100644 --- a/engineering/critique.py +++ b/engineering/critique.py @@ -34,7 +34,7 @@ _DIM_TYPES = { } -async def _check_iso128(backend: "AutoCADBackend") -> list[Issue]: +async def _check_iso128(backend: AutoCADBackend) -> list[Issue]: """Layer lineweights must be in ISO_128_LINEWEIGHTS_MM (or -3=ByLayer sentinel meaning 'inherit').""" issues: list[Issue] = [] @@ -69,7 +69,7 @@ async def _check_iso128(backend: "AutoCADBackend") -> list[Issue]: return issues -async def _check_layer_color(backend: "AutoCADBackend") -> list[Issue]: +async def _check_layer_color(backend: AutoCADBackend) -> list[Issue]: """Engineering-standard layers must keep their canonical ACI color.""" issues: list[Issue] = [] try: @@ -93,7 +93,7 @@ async def _check_layer_color(backend: "AutoCADBackend") -> list[Issue]: return issues -async def _check_construction_left(backend: "AutoCADBackend") -> list[Issue]: +async def _check_construction_left(backend: AutoCADBackend) -> list[Issue]: """CONSTRUCTION layer must be empty before drawing_finalize.""" try: ents = await backend.entity_list(layer_filter="CONSTRUCTION", limit=5000) @@ -113,7 +113,7 @@ async def _check_construction_left(backend: "AutoCADBackend") -> list[Issue]: )] -async def _check_untrimmed_corner(backend: "AutoCADBackend") -> list[Issue]: +async def _check_untrimmed_corner(backend: AutoCADBackend) -> list[Issue]: """Two LINE endpoints within `tol` but not exactly equal usually means a corner where one line overshoots — TRIM/EXTEND/FILLET was forgotten.""" tol = 0.5 # mm — generous; tighten via PlanSpec.notes later @@ -156,7 +156,7 @@ async def _check_untrimmed_corner(backend: "AutoCADBackend") -> list[Issue]: return issues -async def _check_duplicate_entities(backend: "AutoCADBackend") -> list[Issue]: +async def _check_duplicate_entities(backend: AutoCADBackend) -> list[Issue]: """Two LINEs with identical endpoints (in either direction) are duplicates.""" try: lines = await backend.entity_list(type_filter="LINE", limit=5000) @@ -185,7 +185,7 @@ async def _check_duplicate_entities(backend: "AutoCADBackend") -> list[Issue]: return issues -async def _check_dim_overlap(backend: "AutoCADBackend") -> list[Issue]: +async def _check_dim_overlap(backend: AutoCADBackend) -> list[Issue]: """Two dimensions whose insertion points are within `tol` likely overlap.""" tol = 5.0 # mm try: @@ -232,7 +232,7 @@ _FOCUS_DISPATCH = { async def run_critique( - backend: "AutoCADBackend", + backend: AutoCADBackend, focus: list[CritiqueFocus] | None = None, ) -> list[Issue]: """Dispatch each requested focus check and return aggregated issues.""" diff --git a/engineering/gear.py b/engineering/gear.py index 84c85e0..0c115db 100644 --- a/engineering/gear.py +++ b/engineering/gear.py @@ -71,58 +71,6 @@ def _arc_points( return pts -def generate_tooth_profile( - module: float, - teeth: int, - pressure_angle: float = 20.0, - n_points_per_flank: int = 40, -) -> list[tuple[float, float]]: - """Single tooth profile: right flank, tip arc, mirrored left flank.""" - pa = math.radians(pressure_angle) - pitch_r = module * teeth / 2.0 - base_r = pitch_r * math.cos(pa) - outer_r = pitch_r + module - root_r = pitch_r - 1.25 * module - - # Tooth half-thickness angle on the pitch circle. - s = math.pi * module / 2.0 - theta_pitch = s / pitch_r / 2.0 - - # Find involute parameter at pitch circle so we can shift the flank to be - # symmetric about the tooth centerline (y=0 axis after rotation). - if outer_r > base_r: - t_pitch = math.sqrt((pitch_r / base_r) ** 2 - 1.0) if pitch_r > base_r else 0.0 - else: - t_pitch = 0.0 - inv_pitch = math.tan(pa) - pa # involute function at pressure angle - # The flank generated by `generate_involute_flank` starts at theta=0 on the - # base circle. The involute point at the pitch radius makes angle inv_pitch - # (approx). We rotate the flank by -inv_pitch + theta_pitch so the pitch - # crossing sits at +theta_pitch above the tooth-center axis. - rot = -inv_pitch + theta_pitch - - right_flank = generate_involute_flank(base_r, root_r, outer_r, n_points_per_flank) - right_flank = _rotate_points(right_flank, rot) - - # Left flank by mirroring across the X axis (tooth centerline). - left_flank = [(x, -y) for x, y in right_flank] - - # Tip arc connects the outermost right-flank point to the outermost left-flank point. - tip_right = right_flank[-1] - tip_left = left_flank[-1] - a_right = math.atan2(tip_right[1], tip_right[0]) - a_left = math.atan2(tip_left[1], tip_left[0]) - if a_left > a_right: - a_left -= 2 * math.pi - tip_arc = _arc_points(0.0, 0.0, outer_r, a_right, a_left, n=5) - - profile: list[tuple[float, float]] = [] - profile.extend(right_flank) - profile.extend(tip_arc[1:-1]) - profile.extend(reversed(left_flank)) - return profile - - def generate_full_gear_outline( module: float, teeth: int, @@ -200,7 +148,7 @@ def generate_full_gear_outline( async def _draw_helix_symbol( - backend: "AutoCADBackend", + backend: AutoCADBackend, *, cx: float, cy: float, @@ -237,7 +185,7 @@ async def _draw_helix_symbol( async def draw_helical_gear_front_view( - backend: "AutoCADBackend", + backend: AutoCADBackend, *, module: float, teeth: int, @@ -330,7 +278,7 @@ async def draw_helical_gear_front_view( async def draw_spur_gear_front_view( - backend: "AutoCADBackend", + backend: AutoCADBackend, *, module: float, teeth: int, @@ -356,7 +304,7 @@ async def draw_spur_gear_front_view( async def draw_gear_section_aa( - backend: "AutoCADBackend", + backend: AutoCADBackend, *, gear_metadata: dict, x_offset: float, diff --git a/engineering/keyway.py b/engineering/keyway.py index 0d116ff..0ebdc9e 100644 --- a/engineering/keyway.py +++ b/engineering/keyway.py @@ -56,7 +56,7 @@ def keyway_dimensions(bore_diameter: float) -> dict: async def draw_keyed_bore( - backend: "AutoCADBackend", + backend: AutoCADBackend, *, center: tuple[float, float], bore_diameter: float, @@ -110,7 +110,7 @@ async def draw_keyed_bore( async def draw_keyway_section( - backend: "AutoCADBackend", + backend: AutoCADBackend, *, center: tuple[float, float], bore_diameter: float, diff --git a/engineering/layers.py b/engineering/layers.py index 3ea807e..4436350 100644 --- a/engineering/layers.py +++ b/engineering/layers.py @@ -71,7 +71,7 @@ LAYER_SET_REGISTRY: dict[str, list[tuple[str, int, str, float, str]]] = { STANDARD_LINETYPES: list[str] = ["CENTER", "HIDDEN", "PHANTOM", "DASHED", "DASHDOT"] -async def ensure_standard_linetypes(backend: "AutoCADBackend") -> dict[str, str]: +async def ensure_standard_linetypes(backend: AutoCADBackend) -> dict[str, str]: """Idempotently load STANDARD_LINETYPES via backend.linetype_load.""" try: existing = {ln.lower() for ln in await backend.linetype_list()} @@ -92,13 +92,13 @@ async def ensure_standard_linetypes(backend: "AutoCADBackend") -> dict[str, str] return results -async def ensure_engineering_layers(backend: "AutoCADBackend") -> dict[str, str]: +async def ensure_engineering_layers(backend: AutoCADBackend) -> dict[str, str]: """Idempotently create every layer in ENGINEERING_LAYERS via backend.layer_create.""" return await apply_layer_set(backend, "mech") async def apply_layer_set( - backend: "AutoCADBackend", standard: str = "mech", + backend: AutoCADBackend, standard: str = "mech", ) -> dict[str, str]: """Idempotently apply a named layer set ('mech', 'pid', 'iso13567').""" layer_set = LAYER_SET_REGISTRY.get(standard) diff --git a/engineering/section.py b/engineering/section.py deleted file mode 100644 index 794793a..0000000 --- a/engineering/section.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Generic cutting-plane and section-hatch helpers.""" - -from __future__ import annotations - -import math -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from backends.base import AutoCADBackend - - -async def cutting_plane_line( - backend: "AutoCADBackend", - *, - p1: tuple[float, float], - p2: tuple[float, float], - label: str = "A", - layer: str = "PHANTOM", -) -> dict: - """Draw a section line, arrowheads at each end, and 'X-X' labels.""" - x1, y1 = float(p1[0]), float(p1[1]) - x2, y2 = float(p2[0]), float(p2[1]) - - line = await backend.entity_create_line(x1, y1, x2, y2, layer=layer) - - dx, dy = x2 - x1, y2 - y1 - length = math.hypot(dx, dy) or 1.0 - ux, uy = dx / length, dy / length - nx, ny = -uy, ux - - arrow_len = max(length * 0.05, 4.0) - arrow_w = arrow_len * 0.4 - arrows: list[str] = [] - - for tip_x, tip_y, dir_x, dir_y in ( - (x1, y1, -ux, -uy), - (x2, y2, ux, uy), - ): - bx = tip_x + dir_x * arrow_len - by = tip_y + dir_y * arrow_len - wing1_x = bx + nx * arrow_w - wing1_y = by + ny * arrow_w - wing2_x = bx - nx * arrow_w - wing2_y = by - ny * arrow_w - a1 = await backend.entity_create_line(tip_x, tip_y, wing1_x, wing1_y, layer=layer) - a2 = await backend.entity_create_line(tip_x, tip_y, wing2_x, wing2_y, layer=layer) - arrows.extend([a1.handle, a2.handle]) - - text = f"{label}-{label}" - text_h = max(arrow_len, 3.5) - offset = arrow_len * 1.5 - labels: list[str] = [] - for tip_x, tip_y, dir_x, dir_y in ( - (x1, y1, -ux, -uy), - (x2, y2, ux, uy), - ): - tx = tip_x + dir_x * offset + nx * offset * 0.5 - ty = tip_y + dir_y * offset + ny * offset * 0.5 - t = await backend.entity_create_text(text, tx, ty, height=text_h, layer="TEXT") - labels.append(t.handle) - - return {"line": line.handle, "arrows": arrows, "labels": labels} - - -async def apply_section_hatch( - backend: "AutoCADBackend", - *, - boundary_points: list[list[float]], - pattern: str = "ANSI31", - angle: float = 45.0, - scale: float = 1.0, - layer: str = "HATCH", -) -> str: - """Wrapper around backend.entity_create_hatch returning the new handle.""" - info = await backend.entity_create_hatch( - pattern=pattern, - boundary_points=boundary_points, - scale=scale, - angle=angle, - layer=layer, - ) - return info.handle diff --git a/engineering/titleblock.py b/engineering/titleblock.py index b0d87d1..5746526 100644 --- a/engineering/titleblock.py +++ b/engineering/titleblock.py @@ -55,7 +55,7 @@ class TitleBlockMetadata: async def apply_iso_a3_titleblock( - backend: "AutoCADBackend", + backend: AutoCADBackend, *, metadata: TitleBlockMetadata, origin: tuple[float, float] = (0.0, 0.0), diff --git a/engineering/validator.py b/engineering/validator.py index 4137d88..5e22c99 100644 --- a/engineering/validator.py +++ b/engineering/validator.py @@ -62,7 +62,7 @@ class DrawingValidator: async def run( self, - backend: "AutoCADBackend", + backend: AutoCADBackend, *, expected: dict | None = None, ) -> ValidationResult: diff --git a/not.md b/not.md new file mode 100644 index 0000000..2175d7d --- /dev/null +++ b/not.md @@ -0,0 +1,223 @@ +# AutoCAD MCP Pro — Oturum Notları + +Tarih: 2026-06-18 +Kullanıcı: Umutcan Edizsalan (umutcon12@gmail.com) +Hedef: Projeye tam hakim ol → bug/sorun tespit et → geliştirme yol haritası üret → onay sonrası uygula. + +--- + +## YAPILDI ✅ — RECON Workflow (Task #1) + +### Ne yapıldı +- 16 subagent çalıştırıldı: 8 kod-haritalama + 7 web araştırması + 1 sentez. +- Tüm kod tabanı `file:line` hassasiyetinde haritalandı. +- Web araştırması (arXiv, rakip MCP'ler, ezdxf/COM API, ISO standartları) tamamlandı. +- Sentez dosyası: `docs/analysis/00-RECON-dossier.md` (31KB, tam rapor) +- Ham veri: `docs/analysis/_recon_raw.json` + +### Temel Bulgular + +**Gerçek sayılar (dökümanda ne yazıyor vs gerçek):** +| Konu | Dökümanda | Gerçek | +|---|---|---| +| Tool sayısı | "87 tools" | **109 @mcp.tool** | +| Test sayısı | "104 tests" | **184 test fonksiyonu** | +| Section 1 | "(11 tools)" | **12 araç** (drawing_redo fazla) | +| Section 9 | "(5 tools)" | **4 araç** | +| LICENSE dosyası | "MIT — see LICENSE" | **YOK** (legal açık) | +| CI (.github/workflows/ci.yml) | "GitHub Actions CI" | **SİLİNMİŞ** | +| benchmarks/ klasörü | referans var | **YOK** | +| .claude/skills/ ve .cursor/ | referans var | **GIT'TEN SİLİNMİŞ** | + +--- + +## YAPILDI ✅ — FAZ 1: P0 FIX (commit cd6329a, branch fix/p0-com-premium-lineweight) + +- **R1 FİXLENDİ:** 8 premium meta-tool base sınıfına (AutoCADBackend) concrete + backend-agnostik olarak taşındı → COM artık miras alıyor, çökme bitti. Tek backend-özel parça yeni `_create_xline` primitifi. ezdxf'ten ~285 satır tekrar silindi. +- **R2 FİXLENDİ:** `normalize_lineweight()` eklendi (mm-float ↔ hundredths-int konvansiyon çakışması çözüldü); 6 çağrı sitesine bağlandı. ISO-128 lineweight'ler artık 0'a yuvarlanmıyor. +- **R3 FİXLENDİ:** iso128 kritiği artık ateşleniyor (R2 sayesinde); CONSTRUCTION katmanları muaf tutuldu (yanlış-pozitif engeli); yanıltıcı yorum düzeltildi. +- **LICENSE** eklendi (MIT). **ci.yml** geri yüklendi (lint geçici non-blocking; test job gerçek gate). +- **Doğrulama:** 182 passed / 2 xfailed + fonksiyonel test (GEOMETRY lw=50, kritik 0.40mm'yi yakalıyor). + +--- + +## YAPILDI ✅ — FAZ 2: DIAGNOSTICS + IDEAS (workflow'lar) + +- **DIAGNOSTICS** (`docs/analysis/01-DIAGNOSTICS.md`): 31 doğrulandı, 28 onaylandı, 3 reddedildi (R25/R29/R17 adversarial olarak çürütüldü), taramadan 17 yeni bulgu. → 0 CRITICAL (R1-R3 fixli), 2 HIGH (N1 COM hatch, N2 save-format), 14 MED, 10 LOW, 14 SMELL. +- **IDEAS/ROADMAP** (`docs/analysis/02-ROADMAP.md`): 19 fikir Impact×Effort×Diff×Evidence ile skorlandı, 9'u challenge edildi. Sprint 1 (correctness+quick wins) → Phase A (closed-loop moat) → Phase B (ISO production) → Phase C (breadth). +- **İki kritik yeni bulgu:** (1) HTTP auth hiç zorlanmıyor (latent güvenlik), (2) `drawing_finalize` `run_critique`'i HİÇ çağırmıyor — moat finalize'da devrede değil. +- **R7/R8 güvenlik:** otomatik doğrulanamadı (cyber-filter) → savunmacı hardening kalemi olarak Sprint 1'e eklendi. + +**Önerilen sonraki 5:** ① I16 correctness bundle + I19 COM test harness · ② I9 deterministik geometri · ③ I12 HTTP auth + I2 render-as-image · ④ I4 scalar score + I15 birleşik validator · ⑤ I6 GD&T. + +**Durum: Task #4 (konsolide rapor) HAZIR → kullanıcı onayı bekleniyor (FAZ 3 = uygulama).** + +--- + +## KRİTİK BUGLAR (R1-R3 ARTIK FİXLİ — kalanlar FAZ 2 DIAGNOSTICS'te doğrulandı) + +### R1 — KRİTİK: Premium araçlar COM'da çöküyor +- **Konum:** `backends/com_backend.py:1637-1660` +- **Sorun:** 8 premium meta-tool (`drawing_plan`, `drawing_critique`, `point_from_snap`, `construction_xline`, `construction_clear`, `drawing_apply_iso_layers`, `dimension_auto`, `entity_select_smart`) COM backend'inde `NotImplementedError` fırlatıyor. +- **Sonuç:** CLAUDE.md'nin "üretim için olmazsa olmaz" dediği akış, **Windows'taki varsayılan backend'de (COM) çöküyor**. Yani kullanıcı AutoCAD açıp kullandığında bu araçlar çalışmıyor. + +### R2 — KRİTİK: Lineweight sıfırlanıyor +- **Konum:** `backends/ezdxf_backend.py:1034` +- **Sorun:** `int(lineweight)` → 0.50, 0.25, 0.18, 0.13 mm gibi tüm ISO-128 değerleri **0'a yuvarlanıyor**. +- **Sonuç:** Tüm ISO-128 çizgi kalınlığı disiplini sessizce sıfırlanıyor. `drawing_apply_iso_layers` ve `drawing_new` bootstrap'ı da etkileniyor. + +### R3 — YÜKSEK: ISO-128 kritiği kalıcı no-op +- **Konum:** `engineering/critique.py:50` +- **Sorun:** `_check_iso128` içinde `lw <= 0: continue` → R2 yüzünden zaten hepsi 0 → kritik hiçbir zaman ateşlenmiyor. + +--- + +## RİSK KAYITÇISI (Tüm Bulgular, Öncelik Sırasına Göre) + +### KRİTİK (2 adet) +| # | Konu | Konum | +|---|---|---| +| R1 | 8 premium araç COM'da NotImplementedError | com_backend.py:1637-1660 | +| R2 | int(lineweight) → 0, tüm ISO-128 sıfırlandı | ezdxf_backend.py:1034 | + +### YÜKSEK (8 adet) +| # | Konu | Konum | +|---|---|---| +| R3 | iso128 kritiği kalıcı no-op | critique.py:50 | +| R4 | dim_overlap kritiği da no-op (properties yok) | critique.py:194-197 | +| R5 | COM timeout → thread leak + CoUninitialize yok | com_backend.py:415-433 | +| R6 | COM backend'de SIFIR test | tests/ (ComBackend import yok) | +| R7 | Server→sanitizer wiring testsiz | server.py:2035,2052 | +| R8 | LISP aliasing bypass testi yok | security.py:138-179 | + +### ORTA (21 adet) +| # | Konu | Konum | +|---|---|---| +| R9 | entity_array_polar 360° → son kopya 0°=orijinal çakışır | ezdxf_backend.py:946-947 | +| R10 | drawing_finalize fmt belirtmiyor → 3 farklı davranış | server.py:2299 | +| R11 | drawing_save_as(fmt='dwg') ezdxf'de DXF bayt yazar, .dwg ismiyle | ezdxf_backend.py:345-352 | +| R12 | dimension_angular tx/ty yok sayıyor, fixed distance=10 | ezdxf_backend.py:705-722 | +| R13 | entity_offset side_x/side_y her iki backend'de de yok sayılıyor | ezdxf:868-908 / com:860-874 | +| R14 | engineering import guard → None → opaque "NoneType not callable" | server.py:2266,2294 | +| R15 | system_about tool_groups → 15 tool eksik, drifted | server.py:2070-2106 | +| R16 | COM transaction_active sızıntısı hata durumunda | com_backend.py:1402 | +| R17 | _safe_send_command busy-poll STA thread'i bloke ediyor | com_backend.py:1521-1536 | +| R18 | drawing_new bootstrap hatalarını yutuyor, başarı döndürüyor | server.py:280-294 | +| R19 | dimension_aligned/angular kalıcı xfail, kırık araçlar gönderildi | tests/test_dimensions.py:16-27 | +| ... | (+ 10 daha orta öncelikli risk) | | + +### DÜŞÜK (22 adet) +| # | Konu | Konum | +|---|---|---| +| R20 | _registered_tool_count FastMCP iç yapısına bağımlı | server.py:231-234 | +| R21 | Involute dişi geometrisi matematiksel hatalı (trochoid fillet yok) | engineering/gear.py:40-50 | +| R22 | undo/transaction stack paylaşıyor, doc_path sıfırlanmıyor | ezdxf_backend.py:470-484 | +| R23 | COM run_lisp her zaman 'nil' döndürüyor | com_backend.py:1487-1492 | +| R24 | _apply_attrs linetype'ı yüklemeden set ediyor | ezdxf_backend.py:491-501 | +| R25 | block_find_references max_list_limit bypass | server.py:1362-1370 | +| R26 | view zoom ezdxf'de no-op ama success döndürüyor | server.py:1814-1831 | +| R27 | validator title check false-positive ('SPUR' içeren notta) | engineering/validator.py:166-181 | +| R28 | COM HWND palette/splash dialog'u yakalayabilir | com_backend.py:283-300 | +| R29 | ~20 sitede except Exception: pass COM hatalarını yutuyor | com_backend.py:199,244,… | + +### KOD KOKUSU / ANLAMSIZ KOD (53 adet) +- `engineering/section.py` — tamamen dead code, sıfır caller, sıfır test +- `gear.generate_tooth_profile` — dead code (export edilmiş ama hiç çağrılmıyor) +- `BlockInfo.description` field — hiç doldurulmuyor (her iki backend'de) +- `critique.py:53` "normalises to mm" yorumu — FALSE COMMENT +- `engineering/__init__.py:37-39` "Agent B … in flight" / "Task #7/#8" — çok-agent build seam üretime gönderilmiş +- `_LAYER_TEMPLATES` (server.py) ve prompt layer set'leri çakışıyor/kopyalanmış +- `template_list` / `validation_check` saf in-memory, yeterli test yok +- `system_about` static catalog 15 araç eksik +- ... (+ 40 daha smell) + +--- + +## TEST KAPSAMASI (Eksikler) + +1. **COM backend: %0 coverage** — en riskli yer, sıfır test +2. **Server→sanitizer entegrasyon** — araçlar sanitizer'ı gerçekten çağırıyor mu? Test yok +3. **LISP adversarial bypass** — aliasing, UNC path vs test yok +4. **Critique detection paths** — sadece temiz çizimde test edilmiş (hata bulmuyor mu diye değil) +5. `dimension_auto ordinate` — testsiz +6. `dimension_aligned/angular` — kalıcı xfail (kırık gönderilmiş) +7. Pek çok backend metodu hiç test edilmemiyor (system_get/set_variable, drawing_close, view_screenshot, ...) + +--- + +## ARAŞTIRMA ÖZETİ (Ne bulduk) + +### Akademik (arXiv 2025-2026) +- **GeoGramBench (ICLR 2026):** LLM'ler koordinat hesaplamada <%50 başarı → `point_from_snap` yaklaşımımızı doğruluyor, genişletmek lazım +- **CADSmith:** 5-agent döngüsü (Planner/Coder/Executor/Validator/Refiner) + VLM render → Chamfer 38× iyileştirme. **Render → VLM judge döngüsü en yüksek kaldıraç** +- **Self-Improving CAD + FEA:** İterasyon per token'dan çok daha değerli; drawing_critique tek seferlik değil döngü olmalı +- **ProCAD:** Çizmeden önce belirsizlik tespiti → hata oranı %14.6 → %0.9 + +### Rakip MCP'ler +| Rakip | 3D | Render-Critique | Validation Gate | +|---|---|---|---| +| **Bizim (AutoCAD MCP Pro)** | ❌ | ⚠️ finalize-only | ✅ **UNIQUE** | +| build123d-mcp | ✅ | ✅ | ❌ | +| FreeCAD MCP (150+ tool) | ✅ + FEM | ⚠️ | ❌ | +| Blender MCP (22k★) | ✅ | ✅ | ❌ | +| RhinoMCP | ✅ | ✅ | ❌ | + +**Moatımız: Standards-validation gate — kimse yapmıyor.** +**Gerideyiz: 3D, in-loop render-critique, HTTP auth.** + +--- + +## KALAN GÖREVLER ⏳ + +### Task #2 — DIAGNOSTICS Workflow +- Bug'ları `file:line` doğruluğuyla teyit et +- Ölü kodu + anlamsız kodlamaları listele +- Çekişmeli doğrulama (refute) sonrası "onaylanmış bulgu listesi" üret +- **Durum: Bekliyor (RECON tamamlandı, hazır)** + +### Task #3 — IDEAS Workflow +- RECON araştırmasından türetilen fikir adayları oluştur +- Impact × Effort × Differentiation skoru +- Önceliklendirilmiş, kanıta dayalı yol haritası +- **Durum: DIAGNOSTICS bitmesini bekliyor** + +### Task #4 — Konsolide Rapor + Onay +- RECON + DIAGNOSTICS + IDEAS birleştir +- Kullanıcıya sun +- **Onay sonrası** kodlamaya geç (kullanıcı bu tercihi seçti) + +--- + +## DOSYALAR + +| Dosya | İçerik | +|---|---| +| `docs/analysis/00-RECON-dossier.md` | Tam sentez raporu (31KB) | +| `docs/analysis/_recon_raw.json` | Ham yapılandırılmış veri | +| `not.md` | Bu dosya — oturum notları | + +--- + +## ÖNERİLEN FIX ÖNCELİKLERİ (Onay Bekleniyor) + +**P0 — Hemen Çalışmaz Olanlar (Onay Sonrası Fix)** +1. R1: `com_backend.py:1637-1660` → 8 premium aracı COM'a port et +2. R2: `ezdxf_backend.py:1034` → `int(lineweight * 100)` veya enum map ile düzelt +3. LICENSE dosyası ekle (legal açık) +4. CI (ci.yml) geri yükle veya yeniden kur + +**P1 — Kritik Kalite (Onay Sonrası)** +5. `drawing_critique` → iteratif refiner döngüsü +6. Finalize screenshot'ı VLM judge'a besle +7. `drawing_plan`'a pre-flight ambiguity detector ekle +8. COM testleri: mock veya live fixture + +**P2 — Üretim Çizim Kalitesi** +9. ISO-129 toleranslar + GD&T çerçevesi +10. PaperSpace/Layout/Plot desteği (COM) +11. odafc ile gerçek DWG export (ezdxf) + +**P3 — Kapsam Genişletme** +12. 3D solid tier (COM extrude/revolve/boolean) +13. Parametrik yeniden sürüş (driving dims → re-drive tool) +14. HTTP API-key auth middleware diff --git a/server.py b/server.py index 346dc8d..969357d 100644 --- a/server.py +++ b/server.py @@ -287,10 +287,11 @@ async def drawing_new( ) lt_status = await ensure_standard_linetypes(backend) layer_status = await ensure_engineering_layers(backend) - result["bootstrap"] = {"linetypes": lt_status, "layers": layer_status} + result["bootstrap"] = {"ok": True, "linetypes": lt_status, "layers": layer_status} except Exception as exc: - await ctx.warning(f"Engineering bootstrap skipped: {exc}") - result["bootstrap"] = {"error": str(exc)} + await ctx.warning(f"Engineering bootstrap failed: {exc}") + result["bootstrap"] = {"ok": False, "error": str(exc)} + result["status"] = "degraded" return result @@ -2263,7 +2264,7 @@ async def titleblock_apply_iso_a3( ctx: Context = None, ) -> dict: """ISO 7200 / A3 (420x297 mm) title block. Title text is used verbatim.""" - from engineering import apply_iso_a3_titleblock, TitleBlockMetadata + from engineering import TitleBlockMetadata, apply_iso_a3_titleblock backend = _backend(ctx) metadata = TitleBlockMetadata( title=title, drawing_no=drawing_no, part_no=part_no, material=material, @@ -2296,7 +2297,9 @@ async def drawing_finalize( if save_path: validated = validate_path(save_path, allow_write=True) - await backend.drawing_save_as(str(validated)) + from pathlib import Path as _P + _fmt = _P(str(validated)).suffix.lstrip(".").lower() or "dxf" + await backend.drawing_save_as(str(validated), fmt=_fmt) if screenshot_path: validated_shot = validate_path(screenshot_path, allow_write=True) diff --git a/tests/test_dimensions.py b/tests/test_dimensions.py index e3430e3..32f206f 100644 --- a/tests/test_dimensions.py +++ b/tests/test_dimensions.py @@ -13,14 +13,12 @@ async def test_dimension_linear(backend): assert info.type == "DIMENSION" -@pytest.mark.xfail(reason="ezdxf API parameter mismatch in add_aligned_dim", strict=False) async def test_dimension_aligned(backend): info = await backend.dimension_aligned(0, 0, 100, 50, 50, 60) assert info.handle assert info.type == "DIMENSION" -@pytest.mark.xfail(reason="ezdxf API parameter mismatch in add_angular_dim_2l", strict=False) async def test_dimension_angular(backend): info = await backend.dimension_angular(0, 0, 100, 0, 0, 100, 50, 50) assert info.handle diff --git a/tests/test_engineering_gear.py b/tests/test_engineering_gear.py index f0b9a3d..b5f7199 100644 --- a/tests/test_engineering_gear.py +++ b/tests/test_engineering_gear.py @@ -18,7 +18,6 @@ from engineering.layers import ( ensure_standard_linetypes, ) - # --------------------------------------------------------------------------- # Pure math (sync) # --------------------------------------------------------------------------- diff --git a/tests/test_entity_corner_ops.py b/tests/test_entity_corner_ops.py index e991817..4e85118 100644 --- a/tests/test_entity_corner_ops.py +++ b/tests/test_entity_corner_ops.py @@ -4,7 +4,6 @@ Backend coverage: ezdxf only. COM tests need a live AutoCAD instance and are skipped unless explicitly enabled. """ -import math import pytest pytestmark = pytest.mark.asyncio @@ -151,7 +150,7 @@ class TestChamfer: # Tangent points: (96,0) and (100,4) in some order. pts = sorted([tuple(s[:2]), tuple(e[:2])]) expected = sorted([(96.0, 0.0), (100.0, 4.0)]) - for got, exp in zip(pts, expected): + for got, exp in zip(pts, expected, strict=True): assert _close(got[0], exp[0]) and _close(got[1], exp[1]) async def test_chamfer_asymmetric(self, backend): @@ -162,7 +161,7 @@ class TestChamfer: e = cl.properties["end"] pts = sorted([tuple(s[:2]), tuple(e[:2])]) expected = sorted([(96.0, 0.0), (100.0, 6.0)]) - for got, exp in zip(pts, expected): + for got, exp in zip(pts, expected, strict=True): assert _close(got[0], exp[0]) and _close(got[1], exp[1]) async def test_chamfer_trims_source_lines(self, backend): diff --git a/tests/test_meta_tools.py b/tests/test_meta_tools.py index 7bf2e0b..e1f9a3c 100644 --- a/tests/test_meta_tools.py +++ b/tests/test_meta_tools.py @@ -187,9 +187,9 @@ class TestDimensionAuto: assert len(dims) == 2 async def test_unknown_style_raises(self, backend): - l = await backend.entity_create_line(0, 0, 50, 0) + ln = await backend.entity_create_line(0, 0, 50, 0) with pytest.raises(RuntimeError, match="unknown style"): - await backend.dimension_auto([l.handle], style="diagonal") + await backend.dimension_auto([ln.handle], style="diagonal") async def test_empty_handles_returns_empty(self, backend): result = await backend.dimension_auto([], style="chain")