Files
autocad-mcp-pro/docs/analysis/_recon_raw.json
T
Umutcan Edizsalan 0c7f64ceb6 fix(correctness-bundle): I16 correctness fixes + dead code removal + ruff clean
N1: COM hatch non-associative (AddHatch associativity=False)
N2: ezdxf refuses DWG saves with clear error; server.py derives fmt from extension
R19: dimension_aligned dist→distance; dimension_angular correct add_angular_dim_2l args (base/line1/line2/location)
R9: polar array 360° no-duplicate (divisor=n when fill_angle%360<ε)
R13: entity_offset side_x/side_y honored in ezdxf via cross-product (LINE) and distance-to-center (CIRCLE)
R18: drawing_new bootstrap failure surfaces status="degraded" instead of swallowing silently
Dead code: engineering/section.py, gear.generate_tooth_profile, CommandResult, _ACI_NAMES, set_layer_active, ensure_linetypes, ComBackend._batch_mode instance attr, engineering/__init__ build-seam comment
Ruff: 38 auto-fixed + 5 manual → codebase is now ruff-clean; CI lint is now blocking

184 tests pass.
2026-06-18 11:34:30 +03:00

1521 lines
227 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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: <init_error>') 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).<method>(...).",
"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<base_r (common with x=0 profile shift, 20deg PA, low tooth counts) this misrepresents the tooth root. Acceptable as a schematic but not 'production-quality' as CLAUDE.md claims."
},
{
"title": "Tooth flank rotation uses involute-at-pressure-angle as a proxy for involute-at-pitch-radius",
"severity": "medium",
"location": "engineering/gear.py:93-102 and 145-149",
"detail": "rot = -inv_pitch + theta_pitch with inv_pitch = tan(pa) - pa. The involute function evaluated at the pitch circle for a standard gear is indeed inv(pa)=tan(pa)-pa, so this is correct ONLY for the standard (unshifted) case where the base-circle involute reaches the pitch radius exactly at the pressure angle. t_pitch is computed (gear.py:94) but then unused — the code relies on the analytic inv(pa) instead. The dangling t_pitch computation signals intent to do something more general that was never finished; tooth symmetry/thickness will be slightly off if assumptions (full-depth, no profile shift) are violated, and there is no assertion that the involute sample actually crosses pitch_r."
},
{
"title": "Root arc between teeth assumes 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).<method>(...) -> 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`; root-arc gap when `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)."
}