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.
32 KiB
32 KiB
DIAGNOSTICS Report — AutoCAD MCP Pro
Synthesized from the verified recon risk register + fresh-sweep findings. REFUTED / ALREADY-FIXED items are excluded from the ranked body and listed in the appendix. Duplicates between the two sources are merged. Line numbers reflect current code (verified against the working tree).
CRITICAL
None.
HIGH
N1 — COM entity_create_hatch builds an ASSOCIATIVE hatch then deletes its boundary polyline
- Severity: HIGH
- Location:
backends/com_backend.py:659-673 - Description:
mspace.AddHatch(0, pattern, True)requests an associative hatch linked to the appended boundary loop, but the code then callsbnd_pline.Delete()(com_backend.py:673) on that very polyline. In live AutoCAD this either errors onDelete, drops the boundary, or leaves a hatch whose boundary tracking breaks on the next regen/edit. - Fix: Change the 3rd arg to non-associative —
AddHatch(0, pattern, False)— so the computed fill survives deleting the throwaway boundary. If associativity is truly wanted, keep the boundary polyline instead of deleting it.
N2 — drawing_save_as default format diverges (ezdxf "dxf" vs COM/ABC "dwg"); drawing_finalize relies on the default
- Severity: HIGH (merges register R10/R11 with the fresh-sweep duplicate)
- Location: ABC default
backends/base.py:158("dwg"); ezdxfbackends/ezdxf_backend.py:346("dxf"); COMbackends/com_backend.py:516("dwg"); consumed with no fmt atserver.py:2299(drawing_finalize→backend.drawing_save_as(str(validated))). - Description:
drawing_finalizepasses nofmt, so each backend's default decides the output. Two distinct real defects layer here:- ezdxf silently mislabels DWG (R11):
drawing_save_asignoresfmtentirely —doc.saveas(path)derives format from the extension — yet still returns{"format": fmt}. Forfmt="dwg"/"dwt"it writes DXF tag-stream bytes (ezdxf has no DWG/DWT writer) while echoingformat:"dwg". (ezdxf_backend.py:346-353) - COM writes DWG content under a
.dxfname (R10): COM'sSaveAs(path, fmt_map[fmt])honorsfmt, not the extension, so a.dxfsave_path under COM yields DWG bytes in a.dxf-named file.
- Net: identical
drawing_finalize(save_path=...)produces different on-disk formats per backend, decoupled from the extension the user supplied.
- ezdxf silently mislabels DWG (R11):
- Fix: In
drawing_finalize, derivefmtfrom thesave_pathextension and pass it explicitly. Unify the ABC + both impl signatures to one default. In ezdxf, rejectfmt not in {dxf, dwt}(raiseToolErrorfordwg) instead of writing DXF and echoingformat=fmt.
MED
R4 — dim_overlap critique focus is a guaranteed silent no-op (no backend populates the keys it reads)
- Severity: MED (claimed HIGH; downgraded — silently returns
[], no incorrect output) - Location: reads
engineering/critique.py:199-201; extractorsbackends/ezdxf_backend.py:167-169andbackends/com_backend.py:204-244(verified atcritique.py:199-201). - Description:
_check_dim_overlapreadstext_position/defpoint/insertion_point, but the ezdxf DIM branch sets onlyprops["dim_type"]and COM dims get only a genericbounding_box. None of the three keys is produced anywhere, soipis always falsy,dim_pointsstays empty, and the focus always returns[]. - Fix: Populate a dim reference point (text midpoint / defpoint) in both
_entity_infoextractors for DIM types, OR fall back toproperties["bounding_box"]center in_check_dim_overlap.
R9 — ezdxf entity_array_polar overlaps the original on full-circle fill and diverges from COM
- Severity: MED
- Location:
backends/ezdxf_backend.py:947-948 - Description:
step = radians(fill_angle) / max(count-1, 1)with looprange(1, count)places the last copy atstep*(count-1) = 360°for a full ring — coincident with the original at 0°. Also diverges from COM (com_backend.py:921uses AutoCADArrayPolar, spacing360/count):count=4, fill=360→ COM 90/180/270 vs ezdxf 120/240/360.count-1is only correct for partial-arc fills. - Fix:
divisor = count if abs(fill_angle % 360) < eps else max(count-1, 1).
R12 — ezdxf dimension_angular ignores tx/ty and hardcodes distance=10.0
- Severity: MED
- Location:
backends/ezdxf_backend.py:706-723(hardcode at:715) - Description: Accepts
tx/ty(advertised as "text position X/Y" atserver.py:721-722) but callsadd_angular_dim_2l(..., distance=10.0), ignoring them. COM honorstx/tyviaAddDimAngular(com_backend.py:770-784), so identical inputs place the arc differently per backend; the fixed 10.0 mislocates the arc for very large/small parts. - Fix: Pass
location=(tx,ty)toadd_angular_dim_2l, or computedistance=hypot(tx-vx, ty-vy).
R13 / N2-offset — entity_offset ignores side_x/side_y in BOTH backends; side semantics diverge; ezdxf supports only LINE/CIRCLE
- Severity: MED (merges register R13 with the two fresh-sweep offset findings)
- Location: ezdxf
backends/ezdxf_backend.py:869-909; COMbackends/com_backend.py:861-875; ABC sigbackends/base.py:315-319; toolserver.py:859-868. - Description: Both backends accept
side_x/side_yand never read them (dead params; the tool advertises them as "a point on the offset side"). ezdxf hard-fixes the LINE normalVec2(-dir.y, dir.x)*d(:882) and CIRCLEradius+d(:896); every other type raisesRuntimeError("Offset not supported …")(:908) — ARC/LWPOLYLINE/POLYLINE/ELLIPSE/SPLINE unsupported, contradicting the tool's "line, circle, or polyline" claim. COM delegates toent.Offset(distance), whose sign/side convention is not guaranteed to match ezdxf's fixed left-normal — same(handle, distance)can offset to opposite sides per backend. - Fix: Honor
side_x/side_yin both backends (pick the copy whose ref point is nearest the side point, or signdistancefrom the cross product against the offset direction). In ezdxf, support ARC/LWPOLYLINE/POLYLINE/ELLIPSE or drop the documented polyline/side claims. Reconcile so positive distance means the same side in both engines.
R15 — system_about tool_groups static dict omits ~20 registered tools and misfiles one
- Severity: MED
- Location:
server.py:2070-2106 - Description: The hand-maintained
tool_groupsdict omitsdrawing_close,entity_trim/extend/fillet/chamfer, all 7 engineering tools, and all 8 premium tools (~15+ shown), and listsentity_delete_manyunderentity_creationthough it's tagged{entity, modify}withdestructiveHint=True. Livetotal_toolsstays correct; only the per-group breakdown drifts. - Fix: Derive
tool_groupsdynamically from registered tools' tags, or at minimum add the engineering/premium groups, moveentity_delete_many, and add the 5 missing modify/drawing tools.
R18 — drawing_new bootstrap failures return as success
- Severity: MED
- Location:
server.py:282-293; per-item swallow atengineering/layers.py:90-91, 128-129 - Description: Bootstrap is wrapped in
try/exceptthat stores{"error": …}and stillreturn resultrepresenting a created drawing; per-item failures are caught internally as"failed: {exc}"strings. A fully- or partially-failed bootstrap looks like success, so geometry can land on missing engineering layers — a documented precondition violation. - Fix: Surface
bootstrap_ok=False/status="degraded"and/or raiseToolErrorwhenbootstrap=Trueand any linetype/layer reportsfailedor the catch-all fires.
R19 — ezdxf dimension_aligned / dimension_angular ship broken (xfail strict=False masks fix/regression)
- Severity: MED
- Location: markers
tests/test_dimensions.py:16, 23; root causebackends/ezdxf_backend.py:696(dist=) and:711-715(center=/p1=/p2=/distance=) - Description: ezdxf 1.4.3
add_aligned_dim()is(p1, p2, distance, …)but code passesdist=;add_angular_dim_2l()is(base, line1, line2, …)but code passescenter=/p1=/p2=/distance=→TypeError. Both are advertised MCP tools (server.py:695, :714) and recommended in prompt text (:2639).xfail(strict=False)means an accidental fix XPASSes silently. - Fix:
dist→distance;base=(vx,vy),line1=((vx,vy),(x1,y1)),line2=((vx,vy),(x2,y2)). Change markers tostrict=True(or remove) so the suite alerts.
R22 — ezdxf transaction snapshots share the single _undo_stack with undo history; reload leaves _current_layer stale
- Severity: MED
- Location:
backends/ezdxf_backend.py:213(single stack),drawing_undo471-485,transaction_begin/commit/rollback1429-1477,system_status1489 - Description: One
_undo_stackis pushed/popped by both undo and transactions, with no txn id/marker/depth isolation;system_statusreportslen(_undo_stack)astransaction_depth. Adrawing_undosilently consumes an open transaction's snapshot. On reload, undo/rollback re-read the doc but never reset_current_layer, sois_currentreporting goes stale vs the snapshot's$CLAYER. - Fix: Use a dedicated transaction list (or begin-markers/ids) distinct from undo history; on reload re-read
$CLAYERintoself._current_layer.
R5 — COM thread/apartment leak on SendCommand timeout (no CoUninitialize)
- Severity: MED
- Location:
backends/com_backend.py:416-434(timeout path),69-72(_com_init),387-391(disconnect) - Description:
_com_initcallsCoInitialize()as the executor initializer;CoUninitializeappears nowhere in source. OnTimeoutError,stuck.shutdown(wait=False)cannot cancel a worker still blocked inSendCommand(the inline comment admits "Abandon it"), and a fresh executor is built — each timeout permanently leaks oneCoInitialize'd thread + the COM apartment it pinned.disconnect()has the same pattern. - Fix: Bound the leak (track abandoned executors, avoid unbounded re-creation); add per-thread
CoUninitializevia worker-sidetry/finally/thread-local teardown so apartments release when a timed-out thread eventually unblocks.
R6 — COM backend (1651 lines, 85 async methods) has zero test coverage
- Severity: MED
- Location:
tests/conftest.py:5(onlyEzdxfBackendimported); untested logic incl. CMDACTIVE guardcom_backend.py:1474, transactions1397-1419 - Description: No test references
ComBackend/win32/SendCommand/CMDACTIVE. Thebackendfixture instantiates only ezdxf. SendCommand-driven commands, the CMDACTIVE busy-guard + poll loop, and transaction begin/commit/rollback are unexercised, so COM logic can regress silently on non-Windows CI. - Fix: Add
tests/test_com_backend.pyunit-testing the mockable logic (CMDACTIVE guard,normalize_lineweightusage,_safe_send_commandpolling, value coercion) with a mocked COMapp/doc, gated by@pytest.mark.skipifwhenwin32comis unavailable.
N3 — entity_select_smart length_range always rejects ARCs (no backend populates ARC "length")
- Severity: MED
- Location:
backends/base.py:897-904 - Description: The gate fires for
("LINE","ARC")and readse.properties.get("length"), but the ARC extractor branches (ezdxf_backend.py:135-139,com_backend.py:217-222) set only center/radius/angles — neverlength. So every ARC haslength is None → return False, silently excluding all arcs despite the advertised ARC support. - Fix: Populate
props["length"]for ARC (=radius * |Δangle|in radians, with wraparound) in both extractors, OR compute arc length on the fly inentity_select_smart, OR drop "ARC" from the gate and docstring.
N4 — dimension_auto hardcodes layer="DIM", which does not exist in the iso13567 layer set
- Severity: MED
- Location:
backends/base.py:822, 836, 845, 852 - Description: Every
dimension_linearinsidedimension_autopasseslayer="DIM", but the iso13567 set's dim layer is"M-DIMEN-T-N"(engineering/layers.py:57). Afterdrawing_apply_iso_layers("iso13567"), dims land on an auto-created"DIM"(default color 7 / lw -3), bypassing the ISO layer._check_layer_colorwon't flag it because_EXPECTED_LAYER_COLORis built only fromENGINEERING_LAYERS. (The hardcoded"CONSTRUCTION"inconstruction_xline/clear,base.py:743/:754, has the same iso13567 mismatch — its construction layer is"M-CONST-E-N".) - Fix: Resolve the dimension (and construction) layer from the active layer set via a registry lookup keyed on
PlanSpec.layer_set_id(mech→DIM, pid→DIM, iso13567→M-DIMEN-T-N) instead of literals.
N5 — EntityInfo.properties shape diverges by backend (bounding_box, TEXT rotation, MTEXT char_height)
- Severity: MED (merges three fresh-sweep property-parity findings)
- Location:
bounding_box:com_backend.py:192-199(every entity) vsezdxf_backend.py:122(never); TEXT/MTEXT:ezdxf_backend.py:144-152vscom_backend.py:229-237. - Description: COM seeds
properties["bounding_box"]for every entity; ezdxf never adds it. ezdxf TEXT setsrotationand MTEXT setschar_height; COM TEXT/MTEXT set neither (COM MTEXT sets no height at all). Any client reading these keys works on one backend andKeyErrors on the other. - Fix: Pick a contract and enforce it both ways: add
bounding_boxto ezdxf viaezdxf.bbox.extents(or drop it from COM and expose extents only viaanalysis_bounding_box); addrotation/char_heightto the COM TEXT/MTEXT branches (rad2deg(entity.Rotation),entity.Height). Document the props contract.
N6 — COM system_run_lisp always reports result:"nil" (SendCommand is void)
- Severity: MED (duplicates register R23's value-capture half; R23's no-CMDACTIVE-guard half tracked at LOW below)
- Location:
backends/com_backend.py:1488-1493 - Description:
result = doc.SendCommand("(progn …)")thenstr(result) if result else "nil". AutoCAD COMSendCommandis void-returning →resultis alwaysNone→ the tool unconditionally returns"nil"regardless of the actual LISP value. - Fix: Drop the bogus
resultfield, or capture out-of-band:(setvar "USERS1" (vl-princ-to-string <expr>))thenGetVariable("USERS1")after completion.
LOW
R10/R11 residual — covered under N2 (HIGH). No separate LOW entry; the format-default divergence is fully captured there.
R16 — COM transaction_commit/rollback leave _transaction_active stale on error; rollback's UNDO B bypasses the CMDACTIVE guard
- Severity: LOW
- Location:
backends/com_backend.py:1406-1423(commit1406-1413, rollback1415-1423; begin mirror at1402-1403) - Description:
_transaction_activeis set/cleared afterawait self._run(_sync)with notry/finally; if_runraises, the flag stays stale. Rollback's_syncissues rawdoc.SendCommand("_UNDO B\n")(:1419) without the CMDACTIVE pre-check/poll thatsystem_run_command/_safe_send_commanduse — the documented deadlock hazard. - Fix: Clear
_transaction_activein afinallyaround_run; route rollback'sUNDO Bthrough_safe_send_command.
R20 — _registered_tool_count() reaches through FastMCP private attrs; fails to -1 and surfaces it
- Severity: LOW
- Location:
server.py:225-234(private access at:231-232) - Description:
mcp._local_provider._components+ the undocumented"tool:"key prefix, withexcept Exception: return -1.-1is surfaced unguarded astool_count(server.py:1969/1976/1983) andtotal_tools(:2107). A minorfastmcp>=3.0restructuring silently degrades the count to-1. - Fix: Use the public API (e.g.
len(await mcp.get_tools())), making the helper async.
R23 — COM system_run_lisp has no CMDACTIVE guard (deadlock on prompting LISP)
- Severity: LOW (value-capture half merged into N6)
- Location:
backends/com_backend.py:1488-1493 - Description: Unlike
system_run_command(guards at1473-1482) and_safe_send_command(polls at1521-1537),system_run_lispsends raw with no CMDACTIVE pre-check/poll. A prompting form ((command "_LINE"),(getpoint)) leaves CMDACTIVE non-zero and deadlocks the next COM call. - Fix: Route through
_safe_send_command(or add the same pre-send guard + post-send poll).
R24 — ezdxf _apply_attrs assigns linetype without _ensure_linetype_loaded → undefined names silently render Continuous
- Severity: LOW
- Location:
backends/ezdxf_backend.py:501-502 - Description:
_apply_attrsdoesentity.dxf.linetype = linetypewith no load check, unlikeentity_set_properties:981,layer_create:1030,layer_modify:1075.entity_create_lineforwards a documentedlinetypeparam (server.py:446; a prompt even says "use entity_create_line with CENTER linetype"), but_BUILTIN_LINETYPESis only{continuous, bylayer, byblock}, so HIDDEN/CENTER fall through to Continuous. - Fix: Call
_ensure_linetype_loaded(self._require_doc(), linetype)before the assignment.
R26 — ezdxf view_zoom_window is a no-op but reports success; view_zoom_and_screenshot ignores the window region
- Severity: LOW
- Location:
backends/ezdxf_backend.py:1395-1399(zoom no-ops),1401-1425(screenshot ignores window);server.py:1887-1893 - Description: ezdxf zoom ops return
{"ok": True, "message": …}while doing nothing.view_zoom_and_screenshotpasses window coords to the no-op, thenview_screenshotrenders the entire modelspace viadraw_layout(msp)withbbox_inches="tight", ignoringx1/y1/x2/y2— so the image frames the whole drawing despite the docstring promising a window. - Fix: Store a pending crop region in
view_zoom_windowand apply viaax.set_xlim/set_ylim(dropbbox_inches="tight"when cropping); or returnok:Falsefor ezdxf window zoom so success no longer implies framing.
R28 — COM screenshot HWND via title-substring EnumWindows instead of AcadApplication.HWND
- Severity: LOW
- Location:
backends/com_backend.py:284-301(:292) - Description:
_find_autocad_hwndpicks the first visible top-level window whose caption contains"AutoCAD"— a palette, About dialog, or Start tab can win (EnumWindows ~ top-Z first). The COM app object's.HWND(the real main frame) is unused. - Fix: Use
int(_acad_app().HWND); keep EnumWindows only as a fallback.
R21 — Gear flank/root junction is a straight radial segment, not a fillet; radial discontinuity when root_r >= base_r
- Severity: LOW
- Location:
engineering/gear.py:40-50(flank),163-190(root-arc stitch) - Description:
generate_involute_flankprepends(root_r, 0.0)while the involute starts at(base_r, 0.0)— both on y=0, so root→base is a straight radial segment, not a trochoid. Whenroot_r >= base_r(z≥42 @20°, z≥27 @25°, all within the teeth≥6 API limit) no root point is added but the root arc is drawn atroot_r, giving a radial jump in the closed polyline (verified m=2,z=60: 1.118 mm). (The teeth≤2 degenerate case is unreachable — tools enforce teeth≥6.) - Fix: Anchor the flank/root junction at
root_r(radially extend the first involute point, or use a true trochoid below the base circle); skip the root arc whenroot_r >= base_r.
N7 — point_from_snap reads ARC angles via bracket indexing → opaque KeyError instead of the function's standard RuntimeError
- Severity: LOW
- Location:
backends/base.py:629-630, 646-647 - Description: ARC
"end"/"mid"branches usep["start_angle"]/p["end_angle"]direct subscripting, while every other geometry access usesp.get(...)+ descriptiveRuntimeError. If a backend's extraction failed (broad try/except swallows atezdxf:170/com:245), this raises an opaqueKeyErrorrather than the consistent error. - Fix: Use
p.get(...)with the same None-checkRuntimeErrorpattern as_line_pts/_circle_geom.
N8 — PlanSpec stored in _plan_spec is never read; drawing_critique does not "replay" it as documented
- Severity: LOW
- Location: write
backends/base.py:574; never read inengineering/critique.pyrun_critique(234-251); docstringsbase.py:563,server.py:2351 - Description:
drawing_planstoresself._plan_spec = plan; docstrings claim it's "held by the backend and replayed by drawing_critique." Butrun_critiqueand all_check_*never reference_plan_spec/.intent/.scale/.view_count/.dim_style(grep: written once, read nowhere). The committed plan is dead state and the "replay" contract is unmet. - Fix: Implement plan-vs-drawing checks that read
backend._plan_spec(e.g. view_count vs detected views, dim_style vs dims present), or correct the docstrings to stop claiming replay.
N9 — view_zoom_extents/view_zoom_window return different payload shapes (ezdxf adds message, COM omits it)
- Severity: LOW
- Location:
backends/ezdxf_backend.py:1395-1399vsbackends/com_backend.py:1372-1385 - Description: ezdxf returns
{"ok": True, "message": …}; COM returns{"ok": True}with nomessage. Same MCP tool, two shapes — a client keying onresult["message"]works on ezdxf andKeyErrors/None on COM. - Fix: Return identical keys from both backends (always include
message+ anappliedbool).
SMELL
R17 — COM _safe_send_command polling holds the lone STA thread; inner deadline_s decoupled from outer com_call_timeout
- Severity: SMELL (downgraded from MED — functionally correct; inner 8s deadline is tighter than the 60s outer net, 50 ms sleep is a yielding wait, and holding the STA thread is unavoidable under STA)
- Location:
backends/com_backend.py:1498-1542;config.py:38 - Fix (optional): Tie
deadline_s = min(8, com_call_timeout*0.8)so the two stay consistent if the env var changes.
R29 — Broad except Exception guards around optional COM reads
- Severity: SMELL (downgraded — adjacent unguarded calls re-raise genuine disconnects into
_run; most sites log rather than silently pass; reconnect is not bypassed) - Location:
backends/com_backend.py:200, 245, 251, 470, 1447, 1512-1548(~30 sites) - Fix: Narrow to
except (pywintypes.com_error, AttributeError)so genuine logic bugs surface.
S5 — Two parallel, conflicting layer systems
- Severity: SMELL
- Location:
server.py:1647-1688(_LAYER_TEMPLATES) vsengineering/layers.py:14-69(ENGINEERING_LAYERS/LAYER_SET_REGISTRY) - Description:
_LAYER_TEMPLATES(architectural/mechanical/…) uses hundredths-int lineweights, all-Continuous linetypes, no CONSTRUCTION layer;engineering/layers.py(mech/pid/iso13567) uses mm-floats, real HIDDEN/CENTER, ISO-128 lineweights, CONSTRUCTION. The overlapping mechanical/mech sets conflict (HIDDEN/CENTER colors swapped), and_LAYER_TEMPLATESviolates the ISO-128 disciplinedrawing_critiqueenforces. - Fix: Retire
_LAYER_TEMPLATES/template_apply_layersin favor ofLAYER_SET_REGISTRY(or map legacy keys onto it) for one source of truth.
S3 — BlockInfo.description always empty
- Severity: SMELL
- Location: declared
backends/base.py:71; never set inezdxf_backend.py:1196-1202/com_backend.py:1186-1192 - Fix: Populate
description=blk.block.dxf.get("description", "")(ezdxf) + COM equivalent, or drop the field.
S1 — engineering/section.py helpers (cutting_plane_line, apply_section_hatch) are dead
- Severity: SMELL
- Location:
engineering/section.py:12, 65; only re-exported (__init__.py:35, 65-66), never called (gear_draw_section_aabuilds its hatch inline). - Fix: Wire a
section_*MCP tool around them, or delete the module + exports.
S2 — generate_tooth_profile is dead (superseded by generate_full_gear_outline)
- Severity: SMELL
- Location:
engineering/gear.py:74; exported__init__.py:9, 59; duplicates ~80% of the outline math (unused localt_pitchat:94). - Fix: Delete and remove import/
__all__entries, or wire into a draw path.
S4 — Stale "Agent B still in flight" build-seam comment + now-vestigial import guards
- Severity: SMELL
- Location:
engineering/__init__.py:37-39(comment),40-51(try/except guards) - Description: Both
titleblock.pyandvalidator.pynow exist and import cleanly, so the guard's rationale is stale. (Related: R14 — when a guarded symbol is None the tools call it and raise an opaqueTypeError; currently unreachable since modules exist.) - Fix: Delete the build-seam comment; either drop the guards (import directly) or have the tools raise
ToolError("engineering module unavailable: …")when a symbol is None instead of calling it.
N10 — CommandResult dataclass + to_dict never instantiated
- Severity: SMELL
- Location:
backends/base.py:26-38; exportedbackends/__init__.py:2, 8 - Description: Defined and re-exported, but no constructor call exists outside docs; every command return uses a plain dict. CLAUDE.md/README still list it as a shared dataclass.
- Fix: Delete it + its export + doc refs, or actually adopt it as the command return type.
N11 — _ACI_NAMES color-name table defined but never referenced
- Severity: SMELL
- Location:
backends/ezdxf_backend.py:54-57 - Fix: Delete, or wire into
_entity_info_dxf/_layer_info_dxfas acolor_namefield.
N12 — AutoCADBackend.set_layer_active alias has zero callers
- Severity: SMELL
- Location:
backends/base.py:910-912 - Fix: Remove; callers use
layer_set_currentdirectly.
N13 — AutoCADBackend.ensure_linetypes concrete helper is dead (superseded by engineering.layers.ensure_standard_linetypes)
- Severity: SMELL
- Location:
backends/base.py:914-934 - Description: Concrete ABC method with no callers; the real bootstrap path (
server.py:288,layers.py:110) calls the module-levelensure_standard_linetypes, which reimplements the same already_loaded/loaded/failed loop. - Fix: Delete it, or refactor
ensure_standard_linetypesto delegate to it (single owner).
N14 — COM self._batch_mode instance attr written but never read (_regen consults only _COM_STATE)
- Severity: SMELL
- Location:
backends/com_backend.py:363, 889, 907, 916, 933vs_regen180-187 - Description:
_regenguards on_COM_STATE.get("batch_mode")only. The array methods set bothself._batch_modeand_COM_STATE['batch_mode']; the instance attr has no readers and can desync. - Fix: Remove the redundant
self._batch_modeassignments (keep the dict flag), or make_regenread the instance attr and drop the dict — one source of truth.
R27 — Validator step 5 uses substring matching on all TEXT-layer text
- Severity: SMELL (downgraded from LOW — gated by
if helix:, only runs when caller declareshelix_angle; flagging a SPUR title on a declared-helical part is intended; only the over-broad substring scan is a smell) - Location:
engineering/validator.py:159-181(:168,:175) - Fix: Restrict the scan to title-block text and use word-boundary regex (
\bSPUR\b/\bHELICAL\b) instead ofin.
R25-residual — block_find_references INSERT cap
- Severity: SMELL (memory-bug claim REFUTED —
entity_listdefaults tolimit=200, hard-capped; see appendix) - Location:
server.py:1368-1369 - Fix (optional): Warn when INSERT count hits the 200 cap to avoid silently under-reporting references in large drawings.
(a) Count Summary by Severity
| Severity | Count | IDs |
|---|---|---|
| CRITICAL | 0 | — |
| HIGH | 2 | N1, N2 |
| MED | 13 | R4, R5, R6, R9, R12, R13, R15, R18, R19, R22, N3, N4, N5, N6 (14 listed — see note) |
| LOW | 9 | R16, R20, R23, R24, R26, R28, R21, N7, N8, N9 (10 listed — see note) |
| SMELL | 13 | R17, R29, R25, R27, S1, S2, S3, S4, S5, N10, N11, N12, N13, N14 (14 — see note) |
Note: counts reflect merges (R10+R11 → N2; R23 split across N6+R23; R13 absorbs two fresh-sweep offset findings; R10 residual folded into N2). Exact kept-finding totals: HIGH 2, MED 14, LOW 10, SMELL 14 = 40 kept; 3 dropped (refuted/already-fixed appendix below).
(b) Confirmed P1 Shortlist — highest-leverage fixes to tackle next
- N1 — COM hatch associativity/delete bug (
com_backend.py:659-673). FlipAddHatch(…, True)→False. One-line fix, prevents corrupt/invalid hatches in live AutoCAD. - N2 — unify
drawing_save_asformat + derive from extension indrawing_finalize(base.py:158,ezdxf_backend.py:346,com_backend.py:516,server.py:2299). Fixes silent DXF-as-DWG mislabeling and cross-backend format divergence on the primary output path. - R19 — fix the two ezdxf dimension calls + make xfail strict (
ezdxf_backend.py:696, 711-715;tests/test_dimensions.py:16,23). Two advertised tools ship broken; cheap, high-visibility. - R4 — make
dim_overlapactually run (critique.py:199-201+ extractors). Restores a premium-workflow safety check that is currently a guaranteed no-op. - R18 — surface bootstrap failure in
drawing_new(server.py:282-293). Prevents silent production-precondition violations (geometry on missing layers). - N4 — parameterize
dimension_auto/ construction layers by active layer set (base.py:822/836/845/852, 743, 754). Fixes ISO-13567 dims/construction landing on wrong auto-created layers. - R9 — fix full-circle polar array overlap + COM divergence (
ezdxf_backend.py:947). Eliminates duplicate-entity output and a backend behavior split. - R6 — add a mocked COM unit-test suite (
tests/test_com_backend.py). Unlocks regression safety for 1651 lines / 85 methods that currently can't be caught on CI — force-multiplier for every other COM fix above. - R5 — bound the COM timeout thread/apartment leak (
com_backend.py:416-434). Prevents long-session resource exhaustion in live AutoCAD. - R13/N5 —
entity_offsetside params + property-shape parity (ezdxf_backend.py:869-909,com_backend.py:861-875; props extractors). Removes two dead documented params and the cross-backendpropertiescontract drift that breaks downstream clients.
(c) Refuted / Already-Fixed Appendix (checked, not actioned)
- R1 — premium tools missing on COM: ALREADY FIXED in
cd6329a. The 8 premium meta-tools are now concrete methods onAutoCADBackend(backends/base.py); only_create_xlineis backend-specific. - R2 —
int(lineweight)truncation: ALREADY FIXED incd6329avianormalize_lineweight()inbase.py(reconciles mm-float vs hundredths-int). - R3 —
iso128critique no-op: ALREADY FIXED incd6329a. - R8 — (no verification payload supplied; not assessed). Listed only for register completeness.
- R14 — engineering import-guard
TypeError: PARTIAL / not reachable. Bothtitleblock.pyandvalidator.pyexist and import cleanly, so theNone-fallback branch is never taken in the shipped repo. The vestigial guards/comment are carried as S4 (SMELL). - R25 —
block_find_referencesunbounded INSERTs + KeyError: REFUTED.entity_listdefaults tolimit=200(hard-capped at all three layers), the opposite of "unbounded";e.properties.get("block_name")cannotKeyError. Optional cap-warning carried as a SMELL above. - R29 — COM broad
except Exceptionmasks disconnects: REFUTED as stated (reconnect not bypassed; most sites log). Retained as SMELL (narrow the guard types). - R17 —
_safe_send_commanddeadline decoupling as a defect: the defect framing is REFUTED (inner deadline is the tighter, primary one); retained as SMELL. - S4 (
Task #7/#8phrasing): the specific cited phrase is REFUTED (the comment says "Agent B … still in flight," not "Task #7/#8"); the substantive build-seam-comment smell STANDS and is kept as S4.