engineering/mech/draw.py is the only module in the package that touches a
backend: it turns the pure engine's primitive descriptions into entity_create_*
and dimension_* calls, places them in WCS, and writes the part model back onto
the drawing as ACADMCP_MECH XDATA on an anchor POINT. It is not a contract
member and neither are the tools - exactly like gear_draw_* and
titleblock_apply_iso_a3, it composes the generic members, so it is written once
and works on both engines by construction and no capability key is needed. No
new ActiveX call was written, so there was nothing to probe on the live seat;
measured instead that every one of the 23 backend members draw.py calls exists
on ComBackend and that ComBackend has no abstract methods left.
mech_part_draw takes one model - a turned profile or a plate outline plus typed
features - and draws its views in first-angle positions (third on request),
dimensions them and records the placement. mech_view_add reads the model back
out of the drawing and adds a side, top, section (full/half/offset/revolved) or
detail view months later without the caller re-describing the part.
mech_dimension_part lays out ISO 129 dimensions with no overlapping text,
resolves ISO 286 fits against the measured nominal, and moves the holes into a
table past a threshold. mech_hole_pattern exposes the same expansion for a
drawing that was never built from the model - with its own diameter guard,
because Hole.validate needs a part and a standalone pattern has none.
mech_part_inspect reads the payload back and never modifies the drawing.
mech_part_from_spec draws a whole sheet inside one transaction. Every part is
validated before the transaction opens, so a refusal leaves the drawing
untouched and names the path (parts[1]: segments[0].d_outer: ...), and the
rollback is shielded with the shape engineering/pid/spec.py documents: anyio
cancellation is level-triggered, so a bare await in the handler would be
cancelled before the rollback body ran and leave half a sheet inside an open
transaction.
fits goes through dimension_intents(fits=...), the API Task 6 shipped, rather
than being stamped on at draw time: that is the path that validates a key by
name and refuses a fit on a reference dimension or on a hole that moved into
the table. It is scoped per view first - a fit on segment[0] names nothing in
the end view, and refusing there would make a correct three-view sheet
undrawable - and any key no view claimed is handed back to dimension_intents so
the refusal is written in its own vocabulary.
SECTION 21 registers the six tools in the new mech pack, with alias records
(SECTIONPLANE, ARRAY plus shop-floor synonyms in English and Turkish - mil,
kademeli mil, kama, pah, kesit, gorunus), a regenerated tool inventory, the
README release snapshot at 210 tools in 24 groups, and the frozen group-size
snapshot gaining mech: 6. Lean is deliberately untouched here: spec section 11
puts three of these on the lean surface together with group G's two sheet tools,
and Task 15 makes that edit once across all four branches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
SECTION 24 completes at eleven tools with xref_attach, xref_manage,
image_attach, data_extract and drawing_export_dwg, on the four contract members
that genuinely differ per engine -- backends/contracts/refs.py. Everything else
in the sheet track composes the generic members and is written once.
The ActiveX members were read off the AutoCAD 2026 seat's own type library
(C:\Program Files\Common Files\Autodesk Shared\acax25enu.tlb, "AutoCAD 2025
Type Library" 1.0, the library the running 25.x seat registers) before a line of
the COM path was written, and the measurement is in the code beside the calls:
IAcadModelSpace.AttachExternalReference(PathName, Name, InsertionPoint,
Xscale, Yscale, Zscale, Rotation, bOverlay, Password)
IAcadModelSpace.AddRaster(imageFileName, InsertionPoint, ScaleFactor,
RotationAngle)
IAcadBlock: IsXRef (get), Path (get AND put -- so the repath is an assignment,
not a method), Reload(), Unload(), Bind(bPrefixName), Detach(),
XRefDatabase (get)
IAcadDocument.SaveAs(FullFileName, SaveAsType, vSecurityParams)
AcSaveAsType: ac2000_dwg=12 ac2004_dwg=24 ac2007_dwg=36 ac2010_dwg=48
ac2013_dwg=60 ac2018_dwg=64 (=acNative)
The running seat itself was not answering COM while this landed (its window was
Not Responding and GetActiveObject never returned), so the type library -- which
is the same seat's declared surface and reads without a call into the process --
is what the members are measured from; no document of the operator's was opened
or touched. This repository has been bitten four times by a fake that invented a
member, so the COM fakes in the tests model those names and nothing else.
ActiveX angles are radians, so the degree-to-radian conversion happens at that
boundary and nowhere else.
Headlessly, ezdxf.xref.attach carries the attachment (measured on ezdxf 1.4.4:
block flags 20 for an attachment and 24 for an overlay, so "is this an xref" is
flags & (BLK_XREF | BLK_XREF_OVERLAY) -- masking BLK_XREF alone misses every
overlay), add_image_def + add_image the raster, with Pillow reading the pixel
size so the placed image keeps the file's aspect.
Three capability keys, declared in both maps:
dwg_write -- measured absent here (odafc.is_installed() is False), so the
ezdxf entry is a runtime re-evaluation like viewport_render's
matplotlib check rather than a hardcoded False: installing the
ODA File Converter starts the call working with no edit to the
map. The refusal names the install route and drawing_export_dxf.
xref_live -- reload and bind need a live seat's xref manager and are refused
headlessly; list, detach and path are headless and are not
behind the key.
xlsx_write -- openpyxl is optional and absent here; CSV is always available,
so the refusal never blocks the deliverable and says so.
Two numbers the plan predicted wrongly are corrected to what the registry
actually does. `block` outranks `drawing` in _GROUP_TAG_PRIORITY, so the two
xref tools file under `blocks` (9 -> 11) beside block_insert -- which is where
an xref belongs, being a block definition that lives in another file -- and
`drawing` moves 11 -> 12, not 11 -> 14. And `data_extract` must not say "parts
list" in its own text: that is the golden discovery query for
entity_create_table, and a file writer outranking the tool that draws the thing
is a regression the tuning bar catches. It is described by its destination --
a file -- instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
SECTION 24 grows to six tools: revision_add, bom_extract, bom_table,
balloon_add.
bom_extract reads. It walks the INSERTs through the generic members already in
the contract -- entity_list, block_get_attributes, entity_get_xdata -- and
returns parts-list rows from the ACADMCP_MECH std_part payload, falling back to
the block's own ATTRIBs and reporting which of the two each row came from. A
block with neither is skipped rather than guessed at, and the tool never writes
to the drawing.
bom_table draws the rows as a real TABLE, 180 mm wide (ISO 7573: the item list
is as wide as the title block), growing upwards from directly above it with the
heading row against the block and item 1 the row above -- which is what ISO 7573
asks for on a drawing. A TABLE is drawn downwards from its insertion point, so
the upward list is inserted a table height above its anchor with the heading as
the last row; one table_layout feeds both the real TABLE and the pure
table_prims, so the two renderings cannot disagree.
balloon_add draws the ISO 6433 reference -- numeral, leader, dot on the item --
and writes the balloon payload naming its targets. A rerun over the same
targets renumbers that balloon instead of drawing a second one, and an item
number already used for different targets is refused by name with the other
balloon's handle.
revision_add appends a row to the revision block, is idempotent per revision
code (a repeat draws nothing at all), and optionally clouds the changed regions
and tags them with the triangle. Clouds are refused up front, carrying
capability "revcloud", on an engine that has no revcloud member -- the COM
backend -- rather than leaving half a revision behind.
Declared: ISO 7200 does not standardise the revision block's geometry and
ISO 7573 does not fix the item list's column widths or row height, so both
reuse this repository's 180 mm / 7 mm ruling and say so in their SOURCE
docstrings. ISO 6433's "numeral at least twice the dimension character height"
is met by the numeral being 1.4x the balloon radius: the 4 mm default clears
2.5 mm dimension text, and balloon_add's docstring tells a sheet dimensioned at
3.5 mm to pass radius=5.0.
Three corrections to what the plan wrote, each measured rather than assumed:
* extract_records read `block_get_attributes(...)["attributes"]`. Both engines
return the flat {tag: value} mapping itself (ezdxf_backend.py:6687 builds it
from `ent.attribs`, com_backend.py:5703 from `ref.GetAttributes()` as
{TagString: TextString}), so every attribute fallback would have been
silently empty -- measured headlessly, where a PLATE insert carrying only a
DESIGNATION ATTRIB extracted to nothing.
* draw_bom_table now reports `entity_type` / `representation` /
`child_handles`. `entity_create_table` returns EntityInfo(type="TABLE") whose
handle is the first CHILD of a composite on the headless engine, so reading
that handle back gives a rule LINE, not an ACAD_TABLE -- the contract
tests/test_table_leader.py already pins. A caller that means to edit the
table afterwards has to be told which of the two it got.
* add_revision reports one `tags` entry per tag asked for ({at, handles}), not
one per entity: a tag is two primitives, so a flat handle list claimed twice
as many tags as the caller requested.
Discovery: `bom_extract` / `bom_table` / `balloon_add` each entered the index
heavy with "parts"/"list" tokens and took top-1 for the bare query "parts list"
away from `entity_create_table` (a BM25 tie broken alphabetically). Narrowed on
my side only -- the reader speaks of the bill of materials, the drawer names a
table, the balloon names an item -- never by touching the golden case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The plan embeds Task 12's module and tests verbatim, so it still carried the
`_current_space` read and the derivation that put the third-angle cone's short
side away from the circles. Both replaced with what shipped, so a re-run of the
task cannot transcribe the defects back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
engineering/mech/stdparts.py turns the Task 8 tables into real blocks. catalogue()
searches 83 transcribed parts by designation, standard, size or family;
block_spec() emits the typed primitives and DESIG/STD/SIZE/MAT ATTDEFs
block_define takes, in side/top for fasteners and ISO 8826-1 simplified /
ISO 8826-2 detailed for bearings; insert_std_part() defines the block once,
inserts it and writes the std_part ACADMCP_MECH payload the skeleton pins, so
the parts list is a read of the drawing rather than a naming convention.
Threads follow ISO 6410 - minor at 0.8 x major, a thread-length limit line to
the major, a three-quarter minor arc in the end view.
std_feature_draw() adds a thread, a DIN 509 undercut, a DIN 471/472 groove, a
DIN 332 centre hole or an ISO 3601-2 O-ring groove to geometry that already
exists. Its dimensions come from the shared standards registry, never from this
module: an absent table, a size outside coverage or a row missing a dimension is
refused by name, and the DIN 509 relief is drawn from the profile its table
publishes rather than reconstructed here. DIN 332 form R is refused outright.
An unknown designation is refused with the nearest catalogue entries; nothing is
drawn approximately.
SECTION 22 registers the three tools in the new mech pack, with alias records
(INSERT plus shop-floor synonyms in English and Turkish), a regenerated tool
inventory, the README release snapshot at 207 tools in 24 groups, and the frozen
group-size snapshot gaining mech: 3.
Two plan corrections, both verified by running the code:
* the plan's @cad_tool summaries for std_part_insert (118 chars) and
std_feature_draw (130) overran discovery.serialize.MAX_SUMMARY_CHARS = 110, so
tests/test_tool_registry.py::test_no_authored_summary_is_long_enough_to_be_truncated
failed - they would have shipped rendered as "...". Both are shortened to 99
and 104 characters with the same content.
* the plan's file list omitted tests/test_tool_packs.py, which pins the pack
registry, the tool_packs report and the group count. It now carries MECH_TOOLS
and distinguishes ALL_PACKS (declaration order, what "available" reports) from
ALL_PACKS_ENABLED (sorted, what "enabled" reports) - those two lists stopped
being the same list when "mech" was appended after "settings".
No new ActiveX member is called: the module drives block_list / block_define /
block_insert / entity_set_xdata / entity_create_* only. The COM path was still
executed live once on AutoCAD 2026 in a scratch document created and closed by
the probe: the bolt block defined with layers CENTER+GEOMETRY, attributes read
back as DESIG/STD/SIZE/MAT = ISO 4014 - M12x60 / ISO 4014 / M12x60 / 8.8, the
ACADMCP_MECH std_part payload round-tripped through entity_get_xdata,
STD_ISO_15_6205_SIMPLIFIED inserted, and the M20 thread's three primitives
landing on GEOMETRY.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
centre_marks draws ISO 128-23 centre marks or full centre lines and reads
each circle's real centre and radius back out of the drawing through
entity_get instead of being told them - a radius restated from memory is
the silent wrong number this repository keeps refusing to ship. A handle
that is not a CIRCLE or ARC, and a circle in a plane tilted out of WCS XY
(which has no radius in this frame), are both refused by handle before
anything is written. The dash proportions are not transcribed here: the
prims are plain lines on CENTER, whose linetype already carries them.
section_line draws the ISO 128-40 cutting-plane line - wide segments at
the ends on GEOMETRY, the thin long-dash-dot middle on CENTER, an arrow at
each end whose head sits on the line end and points the way the section is
viewed, the same letter beyond both arrows - and returns the five-key
plane dict {p1, p2, label, direction, style} that mech_view_add(kind=
"section") consumes, so the label on the plan and the label on the view
are the same string by construction. A viewing direction parallel to the
plane is refused rather than drawn.
hatch_material hatches through standards.materials.hatch_for, so the
ISO 128-50 map and its refusal stay in one place; scale multiplies the
material's own pattern scale and angle replaces its angle. An unknown
material is refused with the list before any entity is written.
DEVIATION - one defect in the plan's own draw_material_hatch, fixed here
with two tests that fail against the plan's code. It called
entity_create_hatch with ROLE_LAYER["hatch"] without creating that layer,
the same omission review found in Task 10's draw_annotation_prims.
MEASURED live on AutoCAD 2026 (probe: GetActiveObject, Documents.Add(),
doc.Close(False) in a finally): on a scratch document whose layer table is
['0'], msp.AddHatch(0, "ANSI31", False) + AppendOuterLoop + Evaluate()
leaves the AcDbHatch in model space, and the very next statement
hatch.Layer = "HATCH" - which is the one line _apply_entity_attrs runs -
raises com_error (-2147352567, ..., 'Key not found', ..., -2145386476);
model space then still reports [('AcDbHatch', '0')] and the layer table is
still ['0'], so the refusal leaves an orphan hatch on layer 0. After
doc.Layers.Add("HATCH") the same assignment succeeds. drawing_open runs no
bootstrap, which is exactly when a cut face has no HATCH layer to land on.
draw_material_hatch now calls ensure_annotation_layers before creating the
hatch and reports layer / layers_created, as draw_annotation_prims does.
The whole task re-measured live afterwards through ComBackend on a fresh
scratch document (layer table ['0']): hatch_material -> {'ok': True,
'handle': '82', 'pattern': 'ANSI31', 'layer': 'HATCH', 'layers_created':
['HATCH']} with the entity read back as HATCH on HATCH; centre_marks on a
live CIRCLE r=12.5 -> sources [{'handle': '84', 'center': [80.0, 0.0],
'radius': 12.5}]; section_line -> 9 entities and the five-key plane; layer
table after ['0', 'CENTER', 'GEOMETRY', 'HATCH', 'TEXT']; document closed.
tests/test_mech_marks.py: 17 passed. Gates: ruff check clean, inventory
regenerated to 209 tools, test_tool_registry (engineering 12 -> 15, total
206 -> 209), test_discovery_aliases, test_tool_inventory,
test_capability_contract and test_release_consistency all pass. Full
suite: 3343 passed, the 6 pre-existing failures verified identical with
this change stashed, and
test_release_consistency.py::test_document_numbers_match_the_tree
deselected as Global Constraints instructs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
SECTION 24 opens with sheet_frame and titleblock_apply.
engineering/sheet/frames.py transcribes ISO 216's trimmed sizes, ISO 5457's
20 mm filing margin and 10 mm edges, the grid reference system (an even number
of divisions per side, that side of the frame over 50 mm; letters from the top,
numbers from the left), the centring marks (trimmed edge to 5 mm inside the
frame) and the 10 x 5 mm corner trimming marks. Every one of those numbers is
pinned by a test against the clause it comes from, because a wrong margin is
visible on every drawing this server makes.
engineering/sheet/titleblock.py draws the ISO 7200 block -- 180 mm wide, which
is exactly the drawing-space width of a portrait A4, which is why one block
fits every size -- and carries the ISO 128-30 projection-angle symbol. Which
symbol is which is derived rather than remembered: first-angle projection puts
the view seen from the left on the right, and the view of the cone from its
small end is the small circle inside the large one, so first angle draws the
cone left and the circles right.
The old A3 module folds in. apply_iso_a3_titleblock now forwards to
apply_titleblock(size="A3", projection=None, zones=False, marks=False), which is
the geometry it always drew, and tests/test_engineering_titleblock.py passes
unchanged -- that is the regression gate for the fold.
No new ActiveX member is called: both tools reach the drawing only through
contract members the A3 title block has used on both engines since v1.0
(entity_create_polyline / _line / _circle / _text, layout_list,
layout_set_current), so there was nothing to probe live.
Declared, not glossed: the zone divisions drawn here are equal, so ISO 5457's
allowance for shorter corner fields is not implemented; the division counts,
which are what a zone reference quotes, are the standard's. ISO 7200's full
optional-field list is not transcribed, only the fields this block has a cell
for. Coverage is A4-A0; an unknown size is refused by name with the five it has.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
ISO 1302:2002's seven transcribed symbol-proportion rows (text height ->
line width d', short-leg height H1, minimum height H2) drive the 60 deg
vee, the machining-required bar at H1, the machining-prohibited circle
inscribed in the vee, and the all-around circle at the kink; the Ra/Rz
values, the process, the lay symbol and the machining allowance land
around the extension line, which grows with the widest of them. A text
height outside the seven rows is refused by name rather than
interpolated, and ISO 1302 is accepted as an alias designation for
drawings still issued under it - the graphical symbol is the same one.
ISO 2553:2019's reference line, dashed identification line (omitted for a
symmetrical weld, as the standard permits), size written before the
symbol, length and pitch written after it, field-weld flag, all-around
circle and process tail. Six elementary symbols are transcribed - square,
single-V, single-bevel, single-U, single-J, fillet. The rest of the
elementary table is deliberately not shipped: a weld kind outside the six
is refused with the list of the six, because a guessed weld symbol is a
wrong instruction to a welder.
Both tools place the symbol on a plain leader - a line plus an arrowhead
with the proportions the backend's own mleader already draws - rather
than through leader_create_mleader, which refuses empty text and would
hang an MTEXT label off every symbol.
Proportions neither standard dimensions (the all-around circles, the
flag, the identification line's dashes, the text pitch and the character
width estimate) are named constants documented as this implementation's
declared choices, never as transcribed values.
No new ActiveX member is called: both tools compose LINE / ARC / CIRCLE /
LWPOLYLINE / TEXT through the entity-creation contracts both engines
already implement, so no live COM probe was needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The five gaps the plan's reconciliation found, each resolved in the spec:
part placement lives in draw_part(at=, rotation=) so the dataclasses carry no
origin; engineering/sheet/refs.py is gone with the contract correction;
image_attach takes no clip and titleblock_apply no position (the ISO 7200
corner is fixed by the standard) — both recorded as out of 1.6; balloon_add
places one balloon per call against the item numbers bom_extract already
assigned.
Records the four feature tables that ship empty (DIN 509, DIN 471/472,
DIN 332-1, ISO 3601-2): no row could be verified against the standard here, so
the structure, the row-key contract and the refusal ship without rows, and the
features draw from explicit dimensions until the tables are transcribed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Assembles the skeleton and the five drafted parts into one plan, Task 1
through Task 16, each a TDD task with its failing test, its implementation
and its gate.
Groups:
- (main) Task 1 — primitives, the ACADMCP_MECH codec, the standards registry
and the ISO 128-50 material map; the shared foundation the four branches
all consume.
- M (Tasks 2-7) — the part model, the feature catalogue with its DIN/ISO
tables, the view engine (orthographic, then sections and details),
dimensioning, and SECTION 21's six tools.
- S (Tasks 8-9) — the ISO 4014/4017/4032/7089/4762 and ISO 15 tables, and
SECTION 22's three tools.
- A (Tasks 10-11) — ISO 21920-1 surface texture, ISO 2553 welds, centre
marks, the ISO 128-40 cutting-plane line and the material hatch:
SECTION 23's five tools.
- G (Tasks 12-14) — ISO 5457 frames and ISO 7200 title blocks A4-A0,
revisions, the ISO 7573 parts list and ISO 6433 balloons, xrefs, images,
data extract and DWG: SECTION 24's eleven tools and the four contract
members.
- F (Tasks 15-16) — the six mech_* critique focuses, the mech pack, lean,
aliases and golden queries; then the v5 benchmark, the correctness checks,
the live COM smoke and the docs.
Interface pass over the assembled file: View.dims is tuple[DimIntent, ...] as
the skeleton pins it; every XDATA writer calls Task 1's to_values rather than
re-deriving the chunk split from encode; stdparts' five standard features are
STD_FEATURE_KINDS so they do not collide with features.FEATURE_KINDS; the
revision block carries its own ACADMCP_SHEET_REV application instead of a
fourth ACADMCP_MECH kind that the pinned KINDS would refuse; and Task 16
states 39 correctness checks, which is what it registers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The header, global constraints, file structure, the interface block every task
builds against, the group/ownership table and the merge notes. Task sections
are drafted next.
Also corrects the spec's architecture section: the engineering tools compose
the generic contract members the way gear/keyway/titleblock already do, so only
four operations (xref_attach, xref_manage, image_attach, drawing_export_dwg)
need contract members and an implementation per engine.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
One part model — a revolved or prismatic profile plus typed features — and one
view engine that projects it into front/side/section/detail views with hidden
lines, cut faces and dimensions, instead of a generator per part shape. Around
it: the standard-parts catalogue as code inserted as blocks with attributes,
the ISO annotation symbols, and the sheet standard (ISO 5457 frames A4-A0,
ISO 7200 title blocks, revisions, ISO 7573 parts list, balloons, xrefs, DWG).
25 tools in four sections, new pack `mech`, three capability keys, six mech_*
critique focuses, benchmark task `mech_assembly` and six correctness checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
CLAUDE.md gains items 18-20 (styles, page setup & templates, environment),
the grown drawing_settings facade on item 11, lean 55 / 52 and
TOOL_PACKS=all|core,pid,settings, and three conventions: layer states are
ours (ACADMCP_LAYERSTATES XRECORD, mode "xrecord"), page-setup evidence is
the PDF's own /MediaBox, nothing interprets coordinates in UCS.
README token table re-measured on this tree with benchmarks/token_suite.py
(ratio/v1 estimates): full 199 tools / 59,315 tokens, core,settings 190 /
55,593, core 168 / 48,825, lean 55 / 16,360, search 2 / 356; corpus
re-derived from discovery.aliases: 186 AutoCAD command names, 933 synonym
phrases, 204 tools. Styles & standards row, page setup & templates row,
final environment wording, and "Known limitations of the settings track".
CHANGELOG Unreleased: the track E intro, Added (styles, page setup &
templates, environment, drawing_settings growth, benchmark evidence),
Fixed (the Track A hardening list), Changed (three groups, packs, measured
idle cost, release snapshot 204 · 8 · 5 · 3284). Spec status: implemented.
Where the plan's text and the tree disagreed, the tree wins:
mleaderstyle_create is implemented on both engines through the
ACAD_MLEADERSTYLE dictionary and no `mleaderstyle` capability key exists
(spec §4, capability maps), so the docs say so instead of calling it
ezdxf-only; the Task 7 fix (tolerant ACADMCP_PID payloads, crossings
tested against the arc) joins the hardening list the plan left it out of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Task 24 (merge sanity) on main after the five track E groups merged.
- TOOL_PACK_NAMES = ("core", "pid", "settings"); PACK_TOOL_NAMES["settings"]
is exactly the 22 SECTION 20 tools. Styles and page setup stay in core.
- LEAN_TOOL_NAMES += page_setup_apply, batch_plot (53 -> 55).
- _GROUP_TAG_PRIORITY / _GROUP_TAG_LABELS: "style" moved up next to "pid",
"pagesetup" -> page_setup and "environment" -> environment added; the 28
SECTION 19/20 decorators gain their section tag, so _tool_groups() reports
styles 10 / page_setup 6 / environment 22 (23 groups, 204 tools) and
pack == group == section for the settings pack.
- SECTION headers, backends/base.py composition, contracts/__init__.py
exports and the layer_states feature entry were already correct after the
merge (read and confirmed, nothing edited).
- Pack text: config.py, .env.example, README config row; README <sub> line
204 tools in 23 groups; release snapshot 3259 collected tests (measured).
- docs/tool-inventory.json regenerated; registry snapshot re-derived.
- tests: test_tool_packs.py replaced (11 tests), test_capability_contract.py
+3 (composition, the agreed capability flags, layer_states honesty),
golden set +10 tuning cases (81 = 66 tuning + 15 holdout); all ten route
in the top 3 without alias edits.
Deviation from the plan text: TRACK_E_KEYS carries five keys, not six. The
plan's "mleaderstyle" (ezdxf True / com False) contradicts spec §8.1, which
retracted that key because the ActiveX route exists (§4.2); measured on the
tree: capability_keys() == {boundary_trace, chspace, interactive_prompt,
live_application, preferences, revcloud, wipeout}, and
ComBackend.mleaderstyle_create is a native implementation
(backends/com_backend.py:6752, is_capability_default -> False). The test now
also asserts the key is absent.
No new ActiveX call was written in this task, so no live probe was needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Waves 1-4 of the Track A review produced ~170 minor findings that lived
only in the orchestrator's scratchpad. Filed de-duplicated, one line each:
fixed-in-task-N (this track's hardening tasks), fixed-in-track-A (with the
commit), accepted-limitation (with the reason) or deferred-1.7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Live AutoCAD writes a real .dwt through SaveAs(path, ac2018_Template = 66) and
puts the description into the summary Comments; headlessly a .dxf template is
written through drawing_save_as and a .dwt request is refused with capability
dwt_write, naming the DXF route and the COM route. drawing_save_as's "dwt"
entry was 5 (acR13_dxf): a DWT request wrote an R13 DXF under a .dwt name.
Probed live on AutoCAD 2026 (typelib: ac2018_Template = 66, acR13_dxf = 5,
acTemplateDwg absent; the saved file starts AC1032, the document is rebound,
Comments takes). SECTION 19 is now 6 tools; SAVEAS joins SHARED_ACAD_COMMANDS;
templates/README.md documents the two files per template.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
SECTION 19 grows to five tools with drawing_template_list; drawing_new takes a
bundled name (iso_a3_mech, iso_a1_arch, iso_a3_pid, ansi_b_mech, ansi_d_arch)
or a .dwt/.dxf path and reports template: {name|path, path, source}.
scripts/build_templates.py builds templates/*.dxf through EzdxfBackend
(layer set, header DIM variables until group S's styles land, drawing_settings,
sheet tab, ISO 5457 frame on A3, page_setup_apply) and --check pins them
reproducible. One drift the plan's same-process measurement could not see is
fixed at the source rather than masked: ezdxf orders CLASSES by set iteration,
so two processes disagreed on LAYOUT / ACDBPLACEHOLDER; the section is re-keyed
(required list first, then by name) before the save.
The root .gitignore ignores *.dxf as generated output; templates/*.dxf is
un-ignored there, since the bundled set is the one DXF that is committed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
user_pick_point / user_select answer {cancelled: true} on ESC (DISP_E_EXCEPTION)
and {timed_out: true} past COM_CALL_TIMEOUT; preferences_set refuses read-only,
unknown and out-of-range keys before any ActiveX call.
The read-only key is Files.PrinterStyleSheetPath, not the spec's
PrintStyleSheetPath: IAcadPreferencesFiles has no member of that name
(measured live on AutoCAD 2026 / R25.1), and a misspelt key would make
preferences_get() raise on every live call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Summary fields are refused headlessly with capability dwgprops before any
write; custom properties are $CUSTOMPROPERTYTAG/$CUSTOMPROPERTY pairs and
survive save + reopen. Validation is shared, by name, and never str()-coerces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Two review findings, both root causes.
1. `_sync_header_to_dimstyle` copied the DIMSTYLE entry's ezdxf-internal
arrowhead spelling (OBLIQUE) into the DXF-facing $DIMBLK/$DIMBLK1/$DIMBLK2
header, so the saved file named a block it did not hold (the blocks are
_OBLIQUE / _DOT) and `system_get_variable("DIMBLK")` answered "OBLIQUE"
headlessly against "_OBLIQUE" on COM and in the same engine's own
`dimstyle_list` row. The header write now goes through
`reported_arrowhead` — the same read-side rule `_dimstyle_value` applies.
Pinned by header, `system_get_variable`, save/reopen (`ezdxf.readfile`
header equals the block the file holds) and the Restore-to-Standard
empty-string case in `test_builtin_arrowheads_render_save_and_report_one_spelling`.
2. The `mleaderstyle` capability refusal rested on a false engine boundary.
acax25enu.tlb (AutoCAD 2026) exposes IAcadMLeaderStyle with Name,
ArrowSize, LandingGap, TextHeight, TextStyle, ... — exactly what
`Dictionaries.Item("ACAD_MLEADERSTYLE").Item(i)` returns — and
IAcadDictionary.AddObject(keyword, "AcDbMLeaderStyle") creates one.
COM `mleaderstyle_list` now reads the four values off those objects
(`values_available: True`); COM `mleaderstyle_create` validates every value
(`validate_mleaderstyle`, shared with ezdxf), checks the clash and the text
style before AddObject, then writes the four properties. The contract
method is `@abstractmethod` again, the key is gone from both capability
maps, the fake ActiveX surface models the real call shape, and the spec
(§4.2, §8.1, §9) states the ActiveX route.
Live AutoCAD was not running on this machine (GetActiveObject: operation
unavailable; no acad.exe), so the COM route is type-library-verified and
fake-tested, not executed live — scripts/smoke_settings_com.py (Task 24)
owns that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
90 authored rows (type, range/enum, default, meaning, saved-in, per-engine
support, friendly key); every ezdxf claim is checked against the real
backend and the registry-saved set is pinned to what the backend refuses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Layer states are ACADMCP_LAYERSTATES XRECORD chunks (<= 255 ASCII chars each),
in the file and not in AutoCAD's Layer States Manager. Headless view restore
writes the *Active VPORT (ezdxf's header has no $VIEWCTR; measured). UCS is
stored and made current; tool coordinates stay WCS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Two blocking review findings on the standards data layer:
* DIMTP and DIMTM were pinned to ("float", 0.0, 1e6), refusing every signed
deviation with a false "must be at least 0". AutoCAD documents both as
signed and the repo already relies on it: build_dim_override writes
dimtm=-0.022 for ISO 286 p6 and dimtp=-0.020 for f7, and
dimension_linear(fit=...) emits exactly those. A dimension style could
therefore never hold a double-positive or double-negative fit that the same
server draws correctly per dimension, and an agent obeying the message
would ship a sign-flipped tolerance. Both are now ("float", -1e6, 1e6); a
provenance test resolves the p6/f7/H7/h6 overrides build_dim_override
emits through resolve_dimstyle and pins the signs.
* 827719f made check_dim_value return DIMBLK/DIMBLK1/DIMBLK2 in the
canonical _OBLIQUE spelling and added ezdxf_arrowhead without telling
Task 10, whose plan helpers store the value verbatim and read the raw
string back - executed as written the headless drawing raised at render
and at doc.write, a user block passed the data layer and failed at save,
and ezdxf's OBLIQUE-in-session / _OBLIQUE-after-reload split made an
idempotent re-set report a change (spec section 8.3). This commit adds the
read-side twin reported_arrowhead (never raises; a foreign name passes
through), pins it with an in-session-equals-reloaded round trip, and edits
the plan: Task 9 Produces and Task 10 Consumes now name ARROWHEAD_BLOCKS,
canonical_arrowhead, ezdxf_arrowhead and reported_arrowhead; Task 10's
_write_dimstyle_values maps through ezdxf_arrowhead, _dimstyle_value and
_com_dimvar_for_report map through reported_arrowhead, a new
_require_arrowhead_blocks refuses a user block absent from doc.blocks
before dimstyle_create/modify write anything (and rewrites the name to the
block table's own spelling, measured to be what a reload reports), and two
headless tests witness the rendered INSERTs, the save/reopen spelling and
the refusal. The plan's own copy of the range table is corrected to match
the module (arrow kind, signed tolerances). The edited helper block was
executed against ezdxf 1.4.4 before being written into the plan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
EnvironmentContract carries every member of the track E interface; the two
drawing_properties_* members are @capability('dwgprops') in this worktree only
and become @abstractmethod when group C's implementations merge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Skeleton (contracts, interfaces, file ownership, merge notes) plus the
drafted task steps for Track A hardening (H), styles (S), settings (C),
page setup and templates (P), environment (V) and the final tasks (F);
25 cross-group consistency findings reconciled before commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
38 tools in three sections: ISO-25/ANSI dimension, text and leader styles
with drawing_apply_standard; drawing_settings growth, a system-variable
catalogue and document properties; page setup with PDF mediabox read-back,
batch plot and five bundled templates built by the server's own tools;
multi-document on both engines, portable layer states, named views, UCS,
launch, preferences and operator prompts. Track A hardening is wave 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
scripts/smoke_pid_com.py runs the whole track against the live AutoCAD in
a new document. ComBackend.connect() is lazy by design, so the script reads
reachability from system_status()["connected"] rather than from connect()
as the plan sketched, and closes its document without saving unless --keep
is passed. On this machine (AutoCAD 2026, "25.1s (LMS Tech)") it exits 0:
5 nodes, 4 edges, 0 dangling, confidence_min 1.0, 0 critique issues,
2 XDATA chunks — identical to the headless engine on the same spec. The
first attempt exited 2 on a stale win32com gen_py cache (a package folder
holding only __pycache__); an environment fault, no backend change needed.
A/B against v1.5.1 published as ab-v1.5.1-vs-v1.6.0-dev.json: 26/29 ->
29/29, three miss -> pass, zero regressed. check_doc_numbers.py now names
the report and the two row labels in one place (AB_REPORT, AB_*_LABEL),
and README.md / benchmarks/README.md carry the new rows; the v1.5.0 and
v1.4.0 tables stay as history.
README: P&ID row, the showcase and catalogue images, the token table
re-measured (161 / 44,930 by default, 152 / 41,420 with TOOL_PACKS=core,
lean 50 / 14,007), every stale count reconciled (154 -> 166 registered,
149 -> 161 advertised, corpus 158/672 -> 162/731 by the same count, 19 ->
20 groups, 6 -> 8 resources, six-task matrix with pid_roundtrip), a
"Known limitations of the P&ID track" subsection, and the snapshot line at
this machine's 2423 collected tests (CI has not run on this tree; noted in
the changelog with the pre-existing 1369 vs 1363 drift).
CLAUDE.md: section 17, sections 5 and 7 counts, TOOL_PACKS, the two 1.6
contracts and the reader rules. Spec status -> implemented. The plan and
spec markdown were reformatted by ruff (fenced Python only) because
`ruff format --check .` was already red on them at HEAD and the gate
requires the whole tree clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Two authoring slips in the valve x actuator composition:
- The diaphragm actuator's stem stopped at y=4 while the dome's flat base
sat at y=6, so the canonical control-valve symbol (PID_VALVE_*_DIAPHRAGM,
9 catalogue entries) shipped with its dome floating 2 mm above the stem.
- Actuators were placed at a fixed y=4 regardless of the body, so on the
needle body (top 5.6) the piston/motor/solenoid glyphs were drawn over
the knob and the hand bar across the needle's stem; the stem line(0,0,0,4)
also duplicated the needle's own axial line (and the butterfly disc).
Placement is now body-relative: `_body` reports `axis_top` (where its own
geometry on x=0 ends) and the stem runs from there to the glyph base at
`body_top + STEM_CLEARANCE`; the diaphragm dome's base is the stem top
(base + DIAPHRAGM_YOKE), and the FAIL attribute follows the glyph base.
The reference 8x4 bodies keep their published numbers (globe+diaphragm
signal at y=9.0, bbox top 9.0).
Tests: per-combination check that the stem starts at the body's derived
axis top, the glyph's lowest edge equals the stem top, the glyph clears
the body, and the signal port is the topmost edge; plus a catalogue-wide
no-duplicate-primitives gate. 33 combinations fail on the old geometry.
The plan's Step 3 snippet is synced with the fixed module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Plan covers the prerequisite block_insert attribute fix (ezdxf wrapped the
INSERT in an anonymous *U block and lost its ATTRIBs), the two generic
contracts, the 43-symbol catalogue, routing, the ISA-5.1 grammar, the
graph reader, deliverables, six critique focuses, pid_from_spec, TOOL_PACKS,
resources, benchmark v4, and the live COM smoke.
Spec reconciled with the plan: 43 base symbols, FUNC/LOOP at +-2.0,
solid = DXF SOLID entity, shape/direction options, example spec uses an
actuated FCV-101 (a hand valve has no signal port), refine hints in
detail["hint"], preflight LAYER_SET_INTENT_MISMATCH, radial port matching
on the circle.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
Research snapshot of the AutoCAD MCP field (20 READMEs, 2026-09-14), the
seven 1.6 tracks with measurable acceptance criteria, their order
(A -> E -> B -> G -> F -> H -> D), and the tool-pack decision.
Track A: symbol catalogue authored in code (ISO 10628-2 + ISA-5.1), two
generic contracts (block_define, entity_get/set_xdata), nine pid_* tools,
routing, the ISA-5.1 tag grammar, the pid_graph reader, six critique
focuses, TOOL_PACKS, and the test/evidence plan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
The triage over the 43 unaddressed verdicts returned 4 code and 35 doc items.
The code ones shipped in e24b46d; these are the documents.
Twenty-one applied mechanically after a validation pass confirmed each locator
still matched exactly once and each replacement was supported by the tree. Ten
were held back because a re-verify pass found them mutually exclusive -- three
patches rewriting the same benchmarks/README block, three rewriting the same
OCS paragraph -- and applying them in sequence would have corrupted the files.
Those were resolved by hand into one coherent edit each. One was rejected.
The corrections worth naming:
- **The A/B table contradicted itself in three files.** benchmarks/README.md
and CLAUDE.md said 24 checks / 21-24 / 87.5% / "fixed 3" while the suite has
26 and the published JSON says 21/26, 80.8%, five fixed. All three now agree,
and the five are split into the three new-capability `miss -> pass` and the
two repaired-defect `fail -> pass`, which is the distinction the lane exists
to make.
- **"Every number below is produced by a script"** was false twice over:
section 5's nine scores are literals a human assigned, and section 4 now
quotes hand-collected timings. The claim is narrowed to the charts, which
really are all rendered from committed data, and both exceptions are named.
- **The OCS paragraph over-quantified.** TEXT rotation was given as the single
exception; z components and HATCH boundaries are also entity-frame, and the
live backend records `2d_polyline_coordinates_not_verified_against_live_autocad`
in the same reason string. It now points at `ocs_normalized` as the live list.
- **Transactions were shown as bare parity.** The server itself grades them
differently -- `FeatureCapability(True, "undo_mark")` on COM against
`(True, "snapshot")` headless -- and COM rollback sends `_UNDO B` without
confirming it landed. The row says so.
- **"Raw commands: Opt-in"** named a switch that does not exist. Both tools are
registered unconditionally; `DANGEROUS_COMMANDS_ENABLED` only disables the
sanitizer.
- **"Wave A shipped 13 of a planned 39"** hardened an approximation: the only
source writes `~39`.
- **CAD_PROGID** said nothing beyond the connection was tested against
BricsCAD/ZWCAD/GstarCAD, implying the connection was. It was not.
Perf lane reworked against a fresh paired measurement. The old text claimed
"28% slower than v1.4.0, median of three runs" and that
`EZDXF_CALL_TIMEOUT=0` restored v1.4.0's numbers "exactly". Three problems:
`perf_suite` runs each workload once per invocation and cannot produce a
median, "exactly" was refuted by the figures in its own sentence, and the
number moved when the timeout wrapper was rebuilt. It now reports the guard's
cost -- +20.8% and +19.9%, three paired runs, median taken by hand -- says the
figures have no published artifact, and says the wrapper rework recovered about
a third of the v1.5.0 overhead rather than closing it.
perf-ezdxf.json and the perf chart regenerated against the shipped code, and
the README table rebuilt from that artifact so the two cannot drift.
1267 passed, 1 xfailed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>