Files
autocad-mcp-pro/benchmarks/README.md
T
Umut CANandClaude Opus 5.5 17bf3a39ce release: 1.6.0
The v1.6 roadmap in five tracks: 154 -> 247 registered tools in 28 groups.

Evidence re-measured for the release:
- correctness A/B against v1.5.1: 26/48 -> 48/48, 22 fixed, 0 regressed
  (ab-v1.5.1-vs-v1.6.0.json, replacing the -dev report);
- the reference adapter on matrix v7: 21/21 (autocad-mcp-pro.json, which
  was the v3 run of 1.5); the published-report test follows it to v7;
- the 100-point rubric re-scored per category, every withheld point named:
  93 (the competitor rows are their July snapshots, not re-reviewed);
- the perf lane as medians of five alternating runs, beside a v1.4.0
  comparison on one interpreter (perf-v1.4.0-vs-v1.6.0.json).

Two roadmap criteria are not green, and the changelog, the README and the
roadmap say so: criterion 5 is withdrawn (track D, Plant 3D, dropped) and
criterion 7 is not met (creation 1.25x / 1.38x v1.4.0's time, still
1.17x / 1.20x with the timeout off). Carried to 1.7.

README rewritten for 1.6: what is new, the mechanical and architectural
sheets, the rubric and task-matrix charts, the measured limits (including
the four feature tables that ship empty), and the 1.7 roadmap. server.json
describes the 1.6 surface and declares TOOL_PACKS. CLAUDE.md corrects the
ISO 286 letter set and the empty feature tables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-25 00:17:17 +03:00

26 KiB
Raw Permalink Blame History

Benchmarks

Public AutoCAD MCP capability leaderboard

Two evidence layers

This repository publishes two deliberately separate forms of evidence:

  1. Source-reviewed capability benchmark. Nine named AutoCAD MCP projects are scored with a fixed 100-point rubric covering CAD breadth, correctness and delivery, backend reach, engineering production, tests, and security. The dated data lives in source_review.json, and the README graphic is generated—not hand-edited—from that file.
  2. Fixed-task runtime benchmark. Adapters execute the same task matrix and return pass, partial, unsupported, fail, timeout, or not_run. AutoCAD MCP Pro currently has the reference adapter; other projects do not receive runtime scores until an adapter actually runs their public interface.

Regenerate the source-review chart:

pip install -e ".[pdf]"
python -m benchmarks.render_chart

The source-review chart is not presented as a shared live AutoCAD run. Review dates, evidence grades, project URLs, and the boundary are stored with the data.

Fixed-task runtime runner

The runner separates runtime evidence from the source-review rubric. Every adapter receives the same tasks and produces the same closed result enum: pass, partial, unsupported, fail, timeout, or not_run. A timeout is reported per task, and a capability refusal is recorded unsupported rather than fail — "cannot reach this" and "got this wrong" are different findings. Execution is in-process, so adapters that delegate blocking work must provide cancellation-safe task implementations or be wrapped by an external process supervisor. Reports include commit SHA, Python/platform details, backend capability claims, durations, coverage, and hashes for returned artifacts. Unsupported tasks remain in the fixed-matrix denominator with score zero, preventing partial implementations from receiving an inflated score.

python -m benchmarks.run_competitors --list --matrix v7
python -m benchmarks.run_competitors --server autocad-mcp-pro --backend ezdxf --matrix v7 --json
python -m benchmarks.run_competitors --task table_mleader --task hatch_islands --json

The release-machine ezdxf self-check is 21/21 (100.0) on the v7 matrix (--matrix v6 … --matrix v2 reproduce the earlier sets); the published report is results/published/autocad-mcp-pro.json. Repository stars and raw tool counts do not contribute to the score. Adapter registration lives in competitors.yaml.

Matrix v3 (v1.5.0) — five tasks that can fail

v2 scored this server 10/10, and that number carried no information: every task in it exercised something the server was designed around. tasks_v3.py keeps the v2 ten unchanged — their ids and weights key the published v1.4 reports — and adds five chosen because they can fail. Three of them did, while being written:

Task Category Verified against
tool_discovery discovery six AutoCAD command names, each found in the top 3 of the 154-tool registry the harness ranks — all six came back #1 in the published run
token_budget efficiency 40,305 → 356 tokens advertised, against a 2,000 ceiling fixed in advance
hatch_islands hatch 300 filled with the island, 400 ignoring it
selection_filter selection window 1, crossing 2, bounding box 3, polygon 1
measure_from_handle measurement 139.2699 against the 100.0 a vertex shoelace gives

Token counts come from the offline ratio estimator and the report labels them ratio/v1 (estimate); token_suite.py --tokenizer anthropic counts for real.

Two renames from the release plan, and they are the point. tool_discovery_bilingual → tool_discovery: a Turkish query normalizer was written and then removed, because this repository is English throughout and the Turkish was inferred from the author's chat language rather than required by the product. measurement_massprops → measure_from_handle: no centroid or moment-of-inertia tool ships.

The competitor reports are not re-scored. Those servers are pinned at v1.4 commits and were run against the v2 matrix; they have never been asked these five questions. Their scores stand over the ten they ran, and the matrix chart leaves the five new rows blank (not run) instead of scoring them zero — a zero we invented would be indistinguishable from a zero we measured.

--matrix v2 stays available so a v1.4 report can be reproduced rather than only described, and --publish writes the artifact-path-free file that results/published/ holds.

Matrix v4 (v1.6) — the P&ID round trip and the page-setup truth

tasks_v4.py keeps the v3 fifteen unchanged and adds two tasks that can each fail on their own:

Task Category Verified against
pid_roundtrip pid the example spec drawn through pid_from_spec's code, then read back by the graph builder, which never sees the spec: 5 nodes, 4 edges, 0 dangling ends, confidence_min 1.0, zero critique issues, an instrument index of exactly FIC-101 wired to FCV-101, and the two authored line numbers back on the two process lines while the two unnumbered lines carry sequence-built numbers from XDATA
page_setup_truth pagesetup page_setup_apply("Layout1", ANSI_B landscape 1:1) then batch_plot — the PDF's own /MediaBox must parse to 432 × 279 mm (±0.5), a size no fresh document has; then page_setup_apply("Layout1", ISO_A3 landscape 1:1) and batch_plot again — 420 × 297, the setter's changed.size_mm must read [[432, 279], [420, 297]], the page-setup list must say ISO_A3 / landscape / [420, 297], and the ctb must be a known one. A setter that writes nothing fails at the first PDF (tests/test_benchmark_v4.py stubs it to prove that)

The competitor reports carry no result for either, for the same reason they carry none for the v3 five. --matrix v3 reproduces the v1.5 set exactly.

Matrix v5 (v1.6) — the mechanical sheet

tasks_v5.py keeps the v4 seventeen unchanged and adds roadmap criterion 3:

Task Category Verified against
mech_assembly mech a flange-coupling A3 sheet built through the code the tools call — apply_titleblock (ISO 5457 frame with zones and marks, ISO 7200 title block), a hub drawn by draw_part with a full section by add_view and chain dimensions by dimension_part, a flange with its bolt-circle end view, two ISO 4014 M12x60 bolts and two ISO 4032 M12 nuts by insert_std_part, the ISO 7573 parts list read off the drawing by extract_records and drawn by draw_bom_table, and one balloon per row linked to that row's inserts by add_balloon. The sheet is saved, then gated on two numbers that cannot be negotiated: run_critique(focus=None) returns zero issues and combine(validator, critique) scores at least 90. The focuses that fired come back in the metrics, and tests/test_benchmark_v5.py drops one balloon to prove the gate fails and names mech_bom_balloon_mismatch

Nothing in the task tidies up after the tools: the view engine draws the ISO 128-23 centre lines of every circle it emits, so the sheet is clean because it was drawn clean. Building it found four defects that every headless test had missed — the headless diameter text on the wrong side of the chord, the validator reading a fastener designation (M12x60) as a fake dimension, and on the live seat a missing MECH layer and a parts list that could not see a BLOCKREFERENCE; each is fixed with a test. The competitor reports carry no result for it.

Matrix v6 (v1.6) — the architectural plan

tasks_v6.py keeps the v5 eighteen unchanged and adds track F's evidence:

Task Category Verified against
arch_roundtrip arch the plan of engineering/arch/spec.py::EXAMPLE_SPEC — a 9 × 6 m brick ring at 250 mm split by a 100 mm AAC wall, an entrance door, an interior door, two windows, a straight stair, two labelled rooms, the exterior chains and the door / window / room schedules — drawn in one transaction by draw_plan_from_spec, then read back without the spec: rooms_detect on the wall layer must find each labelled room within 0.1 % of the area its label carries and of the net floor computed by hand (4825 × 5750 = 27 743 750 and 3825 × 5750 = 21 993 750 mm²), schedule_rows must list exactly D1, D2 and W1, W2, and the saved plan is gated like mech_assembly: run_critique(focus=None) returns zero issues and combine(validator, critique) scores at least 90. tests/test_benchmark_v6.py removes one room label to prove the gate fails and names arch_room_unlabelled

The competitor reports carry no result for it.

Matrix v7 (v1.6) — understanding foreign drawings

tasks_v7.py keeps the v6 nineteen unchanged and adds track H's evidence, both on the synthetic plant pair of tests/fixtures/plant_pair.py (invented names; its truth is computed from the positions it placed, never typed):

Task Category Verified against
takeoff_roundtrip plant scale_check(pid, layout) must say schematic (room 1 is a 1.3× stretch, room 2 is rearranged: 21.8 % of the 55 tag pairs within ±10 % of the median ratio); pipe_rows(..., length_source="auto") must report all nine runs with the truth's status, each routable run's layout length equal to the rectilinear MST of its tags to a micron (PR1 5500, PR2 13200, CS1 20700 mm, …), its size and service as drawn — the CIP return drawn on the supply layer included — and both sizes across the reducer; cable_rows must give M11 / T102 / M12 / T202 their panels, 6900 / 14100 / 15800 / 8000 mm and 9 / 17 / 19 / 10 m after the 20 % allowance, the heater's power empty. tests/test_benchmark_v7.py deletes the wiring callouts and the gate names all four loads
understand_foreign understand describe(layout) must infer millimetres and warn about INSUNITS (the file says inches), name the stray line's handle in its extents and report two clusters (the plan is drawn twice, 100 m apart); describe(pid) must classify P_product piping, P_cipsupplyline, P_cipreturnline, P_ijswater and E_power as product, CIP supply, CIP return, ice water and electrical. tests/test_benchmark_v7.py makes the layout declare millimetres and the units gate fails alone

The competitor reports carry no result for them.

Live competitor lane (v1.4)

Two competitor adapters now execute the exact same task matrix, black-box over MCP stdio, against commits pinned in competitors.yaml:

Server Matrix Pinned Score Pass Coverage
autocad-mcp-pro (reference) v7 (21) 1.6.0 tree 100.0 21/21 100%
beiming183-cloud/AutoCAD-MCP v2 (10) 11f7c47e 50.0 5/10 50%
puran-water/autocad-mcp v2 (10) 95476a33 45.0 4/10 50%

The competitor rows are the v1.4 runs, unchanged. Read the matrix column before the score column: the denominators differ because the eleven tasks added since v2 came after those runs and have not been put to them.

Method and boundaries:

  • benchmarks/competitors_env.py clones the pinned SHA into benchmarks/.competitors/<id>/ (gitignored), builds an isolated venv, and installs the competitor from its own pyproject.toml.
  • benchmarks/adapters/mcp_stdio.py drives the competitor exactly like an MCP host (fastmcp Client over stdio). No competitor code is imported.
  • Task playbooks call each server's own documented tool contract, read from the pinned source (consolidated operation + payload tools; beiming's doc_id/expected_revision discipline is honored via transaction(operation="context")).
  • Verification never trusts the competitor's response: every geometry claim must survive save/save_as_dxf and re-opening the DXF with ezdxf inside the harness.
  • Tasks with no documented equivalent are reported unsupported with the reason string; they stay in the fixed-matrix denominator at score zero.
  • This lane is headless (ezdxf backends only). File-IPC / live-AutoCAD lanes (including COM-only servers such as best-cad-mcp or daobataotie/CAD-MCP) require a local AutoCAD session and are out of CI scope by design.

Published machine-readable reports (artifact paths sanitized to filenames) live under results/published/; the README charts are regenerated from them:

python -m benchmarks.run_competitors --server puran-water-autocad-mcp --backend ezdxf --matrix v2
python -m benchmarks.run_competitors --server beiming183-autocad-mcp --backend ezdxf --matrix v2
python -m benchmarks.render_live_chart     # score bars
python -m benchmarks.render_matrix_chart   # tasks x servers status heatmap

--publish <path> writes the same report with artifact paths reduced to filenames, which is how results/published/ is produced; the sha256 stays, because that is what makes an artifact checkable.

Headless performance lane

perf_suite.py measures wall time on fixed workloads through the same backend methods the MCP tools call (server-side overhead included): 2,000 individual line creates, a 10,000-entity build → DXF export → reopen roundtrip, a region query over 10,000 entities, and a full premium quality pass (ISO layers, geometry, dimensions, complete critique). The report records the machine fingerprint; numbers move with hardware, workload definitions do not. Self-measurement only — competitor servers would pay an extra stdio serialization cost that in-process runs do not, so no cross-server timing claims are made.

python -m benchmarks.perf_suite --out benchmarks/results/published/perf-ezdxf.json
python -m benchmarks.render_perf_chart

v1.4.0 against v1.6.0, and what the call-timeout guard costs

The published v1.4 report was recorded on CPython 3.14 and the later ones on 3.11.15, so comparing those files directly shows an apparent speedup that is almost entirely the interpreter. The comparison that means something runs one interpreter and one ezdxf over each tree's own code: 1.6.0's perf_suite.py (v1.4.0 had none) against a v1.4.0 worktree, against 1.6.0, and against 1.6.0 with the guard off — five rounds, the three configurations alternating within a round. Medians, from results/published/perf-v1.4.0-vs-v1.6.0.json, which keeps every run:

Workload v1.4.0 v1.6.0 v1.6.0, EZDXF_CALL_TIMEOUT=0
create_lines_2k 244.5 ms 306.1 ms (1.25×) 284.9 ms (1.17×)
roundtrip_10k 1,847.9 ms 2,553.5 ms (1.38×) 2,216.4 ms (1.20×)
region_query_10k 665.2 ms 712.2 ms (1.07×) 783.9 ms (1.18×)
premium_pass 150.6 ms 65.3 ms (0.43×) 69.9 ms (0.46×)

The guard explains part of the creation cost, not all of it. v1.5.1 rebuilt the wrapper around one asyncio.timeout_at instead of two asyncio.wait_fors (wait_for wraps its awaitable in a Task, so the old shape created two extra Tasks and two timer handles per call), which recovered about a third of its overhead; the rest is the Task the abandon-on-timeout design needs. With the guard off, creation is still 1.17× and 1.20× v1.4.0's time. A cProfile of the event-loop thread over the 2,000 creates counts 491,071 function calls on 1.6.0 against 319,038 on v1.4.0: per-call work added since 1.4 that 1.6 has not removed. The region-query row, whose guard-off median is the slower one, is this machine's noise floor (about ±10 %).

Roadmap criterion 7 — headless creation throughput back to v1.4.0's numbers — is therefore not met and moves to 1.7. Earlier versions of this page said that EZDXF_CALL_TIMEOUT=0 returned creation to v1.4.0's numbers; measured this way, it does not. The guard stays: it is what stops one hung ezdxf call wedging a server whose document lock is a single asyncio.Lock.

perf-ezdxf.json, the chart's input, is the median of the five default-configuration runs, with every run kept in runs_ms.

Reproduce (the v1.4.0 tree needs 1.6.0's benchmarks/perf_suite.py copied in):

git worktree add ../v140 v1.4.0 && cp benchmarks/perf_suite.py ../v140/benchmarks/
for i in 1 2 3 4 5; do
  python -m benchmarks.perf_suite --json > main-$i.json
  EZDXF_CALL_TIMEOUT=0 python -m benchmarks.perf_suite --json > off-$i.json
  (cd ../v140 && python -m benchmarks.perf_suite --json) > v140-$i.json
done

The profile, run from each tree's root:

import asyncio, cProfile, pstats
from backends.ezdxf_backend import EzdxfBackend


async def work():
    b = EzdxfBackend()
    await b.connect()
    await b.drawing_new()
    for i in range(2000):
        await b.entity_create_line(0, i, 100, i)
    await b.disconnect()


pr = cProfile.Profile()
pr.enable()
asyncio.run(work())
pr.disable()
print(pstats.Stats(pr).total_calls)

Correctness A/B suite

correctness_suite.py is a set of deterministic, headless (ezdxf-backend) checks that exercise real drawing operations. compare_versions.py runs the same suite against an older git ref and the current checkout, each check in its own subprocess so a hard crash (e.g. a matplotlib-in-thread SIGSEGV) is recorded as a miss rather than taking down the run.

Reproduce:

python benchmarks/compare_versions.py            # current tree vs origin/main
python benchmarks/compare_versions.py v1.0.0     # vs a tag/ref
python benchmarks/compare_versions.py --json results.json

Result — v1.6.0 vs v1.5.1 (release gate)

48 checks, ezdxf backend, one subprocess per check. Machine-readable report: results/published/ab-v1.5.1-vs-v1.6.0.json.

Version Checks passing Pass rate Fixed Regressed
v1.5.1 (baseline) 26 / 48 54.2 % — —
v1.6.0 (this release) 48 / 48 100 % 22 0

The twenty-two are all miss → pass — v1.5.1 has none of the methods. Five are track H: scale_check_detects_schematic (the synthetic P&ID is schematic against its layout; a uniform 2× copy is to scale with factor 2), pipe_takeoff_rmst_exact (eight routable runs, each exactly the rectilinear MST of its tags), cable_takeoff_roundup (9 / 17 / 19 / 10 m, the unstated power left empty), diff_detects_known_edits (five known edits and nothing else) and topology_known_defects (one near miss, one crossing, a clean T). Four are track F: arch_junction_l_t_x (L, T and X junction outlines against hand-computed corners and wall areas), arch_room_area_net (a 4 × 5 m room between 200 mm walls is labelled 20.00 m², not its 21.84 m² axis area), arch_opening_cuts_wall (both faces interrupted across a door, two jambs) and arch_rooms_detect_foreign (plain lines on a WALLS layer read as 20 and 15 m² at confidence 0.6, nothing written). Seven are tracks B and G: mech_part_roundtrip (a part read back from its own ACADMCP_MECH XDATA equals the part drawn), mech_section_hatch_area (a sleeve 60 long, ⌀40 outside, ⌀20 bore: a full section cuts two 60 × 10 faces, 1200 mm² exactly, measured out of the drawing by entity_measure — never from the code that drew it), mech_iso286_on_dimension (ISO 286-1: 40 H7 is +0.025 / 0, and the fit survives layout_dimensions), mech_thread_unrepresented_is_caught (the ISO 6410 focus fires on a thread drawn as a plain circle and stops firing once the thin ¾ arc is there — the mechanical twin of dim_overlap_critique_fires), std_part_iso4014_m12 (ISO 4014:2011 table 1, M12: s = 18, k = 7.5, matched by value; the inserted block carries its designation back), sheet_frame_iso5457_a3 (the frame runs 20,10 to 410,287 inside the 420 × 297 trimmed sheet) and bom_balloon_link (two identical bolts collapse into one row of quantity 2 with a stable item number, and that row's balloon carries it). The other six are the three P&ID checks track A added (pid_block_define_attdef_roundtrip, pid_tag_parse_fic, pid_graph_edge_count) and the three settings checks of track E (settings_dimstyle_iso25_values: the ISO-25 preset reaches the DIMSTYLE table with ISO 129-1's numbers, read back through ezdxf and not through the lister; settings_layer_state_roundtrip: save → change → restore puts the layer table back and the state survives save/reopen as an XRECORD in the file; settings_pdf_mediabox_a3: the plotted PDF's own /MediaBox reads 420 × 297 mm). Every one of the 26 checks it was released on still passes, so the five tracks added capability without moving a number it had already earned.

Result — v1.5.1 vs v1.5.0

26 checks at the time, ezdxf backend, one subprocess per check. Machine-readable report: results/published/ab-v1.5.0-vs-v1.5.1.json.

Version Checks passing Pass rate Fixed Regressed
v1.5.0 (baseline) 24 / 26 92.3 % — —
v1.5.1 (that release) 26 / 26 100 % 2 0

v1.5.0 fails diameter_dim_measures_the_diameter and radius_dim_ignores_the_leader_length: it has both methods and gets both wrong. That is the defect 1.5.1 exists to fix and the reason 1.5.0 is yanked.

Result — v1.5.1 vs v1.4.0

Same 26-check suite, older baseline: results/published/ab-v1.4.0-vs-v1.5.1.json.

Version Checks passing Pass rate Fixed Regressed
v1.4.0 (baseline) 21 / 26 80.8 % — —
v1.5.1 (that release) 26 / 26 100 % 5 0

This row used to read v1.5.0 | 24 / 26 | 92.3 % | 5, which was impossible on its face — 21 passing plus 5 fixed is 26, not 24 — and it contradicted the report it linked. The cause is worth recording: the file named ab-v1.4.0-vs-v1.5.0.json was regenerated from a tree that already carried the dimension fixes, so it never measured the v1.5.0 tag. The v1.5.0 tag really does score 24/26, which is what the v1.5.0-baseline table above reports. This table now names the tree it actually measured.

The pass rate is against the 26-check current suite; v1.4.0 passed 21/21 of the checks that existed when it shipped. The five splits into two kinds, and the difference is the whole reason this lane distinguishes miss from fail:

  • Three are new capability. v1.4.0 has no entity_measure and no boundary tracing, so it misses hatch_area_subtracts_its_island, boundary_area_agrees_with_measure and two_vertex_circle_has_area by not having the method (miss → pass). They are in the suite because all three broke during v1.5.0 development and nothing outside their own unit tests would have caught it.
  • Two are repaired defects. v1.4.0 and v1.5.0 both have dimension_diameter and dimension_radius and both get them wrong (fail → pass): leader_length, a text placement, was being measured as geometry, so at default settings every diameter callout came out 2 × leader_length too large. Only 1.5.1 passes them.

The 21 pre-existing checks are unchanged and all still pass: the discovery layer, cad_batch, the 19-module contract split, the layout/viewport family and Wave A landed with zero correctness regressions.

Result — v1.4.0 vs v1.3.0

21 checks. Report: results/published/ab-v1.3.0-vs-v1.4.0.json.

Version Checks passing Pass rate Fixed Regressed
v1.3.0 (baseline) 21 / 21 100 % — —
v1.4.0 21 / 21 100 % 0 0

Result — v1.1.0 vs v1.0.0 (origin/main, commit 15fa2bc)

21 checks, ezdxf backend, CPython 3.14, one subprocess per check.

Version Checks passing Pass rate
v1.0.0 (public release) 8 / 21 38.1 %
v1.1.0 (this release) 21 / 21 100 %

13 defects fixed, 0 regressions — a 2.6× higher correctness pass-rate (+61.9 pts).

Fixed (failed on v1.0.0, pass on v1.1.0):

Check Category What it proves
dim_aligned_no_error Dimensions aligned dim no longer raises TypeError
dim_angular_no_error Dimensions angular dim no longer raises TypeError
array_polar_360_distinct Modify full-circle array no longer duplicates the original
point_intersection_line_line Geometry deterministic line/line intersection exists & is correct
point_tangent_external Geometry external-point tangent is perpendicular & on-circle
arc_has_length Query ARC carries a length property
arc_select_by_length Query entity_select_smart length_range selects arcs
ezdxf_bounding_box Query bounding_box populated (COM/ezdxf parity)
mtext_rotation_roundtrip Entities MTEXT honors caller rotation
screenshot_png Render headless render returns a valid PNG (no GUI-thread crash)
dim_overlap_critique_fires Quality gate dim_overlap critique is live (was a no-op)
iso13567_dim_layer Quality gate dimension_auto lands dims on the active set's layer
gear_no_self_overlap Engineering gear outline never dips inside the root circle (z ≥ 42)

Passing on both versions (6 core ops + 2 others) keep the suite honest — it is not a cherry-picked list of failures; the baseline genuinely does basic CAD work.

Caveats / honesty notes

  • The baseline is origin/main (the public v1.0.0). All four audited fix sprints land between it and v1.1.0 — see docs/analysis/.
  • screenshot_png on the baseline is host-dependent: it is recorded as a miss here because the default matplotlib backend is a GUI backend and the render runs off the main thread. On a host where matplotlib defaults to Agg, the baseline may not crash. v1.1.0 forces Agg and is deterministic.
  • center_linetype_applied passes on both because it only checks the linetype attribute string (set on both); the v1.0.0 "renders as Continuous because the linetype was never loaded" defect is not observable through EntityInfo.
  • COM-backend behaviour is not covered here (no live AutoCAD); see the mocked-COM suites in tests/.

Model-by-model tool-call success rates remain future work; scale/throughput numbers live in the headless performance lane above.