Commit Graph
51 Commits
Author SHA1 Message Date
Umut CANandClaude Opus 5.5 27d44e909e merge: track H group N
The merge agent stopped this branch on an add/add conflict it had not been
told to expect: groups U and N each created tests/test_understand_tools.py.
Resolved by hand:
- tests/test_understand_tools.py stays U's (the drawing_understand wire
  tests), and N's takeoff wire tests move whole to
  tests/test_understand_takeoff_tools.py. Each file keeps its own client
  fixture: U's opens a fresh document, N's reads files only.
- server.py: SECTION 26 (4 tools, U then D) is kept as merged, and SECTION
  27 (Plant Takeoffs, 2 tools) follows it whole, above the RESOURCES
  banner.
- discovery/aliases.py: additive; the two takeoff records follow the four
  readers.
- tests/test_tool_registry.py: all three branches' paragraphs are kept.
  The snapshot is recomputed from the live registry: analysis 18, 247
  tools. Task 11 ranks `understand` and `plant`.
- docs/tool-inventory.json regenerated (247 tools, 26 groups); README
  counts are 247 registered / 242 advertised. The collected-tests figure
  is left to Task 11.

ruff check passes. Suite: 4919 passed, 1 xfailed. The one deselected test
is the collected-tests figure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 12:48:12 +03:00
Umut CANandClaude Opus 5.5 cfcc8ce6bb merge: track H group D
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 12:32:37 +03:00
Umut CANandClaude Opus 5.5 459d3b9228 feat(understand): cable_takeoff - one row per load, rounded to the metre
cable_rows reads each load's power, phases, voltage, neutral and VFD from the
P&ID's electrical texts near its tag, and its panel from the layout's wiring
callouts (else the P&ID's), names normalised CP 1 -> CP-1. The length is the
Manhattan distance on the layout from the tag to its panel, and the cable
ROUNDUP(length x (1 + allowance)) to the metre, on the length rounded to
1e-6 m so binary noise never buys a metre. Power is never invented: a load
with no stated kW keeps an empty cell and an open item. Without a layout the
lengths stay empty with an open item per row. Room and panel totals count a
panel package's power once, never its children's as well; the rule is on the
method sheet. Sections only from the caller's section_rules - no standard
table exists here.

The workbook gains the cable shape (Metraj, Ozet by room and panel, Kontrol
as the open-items list, Metodoloji) in the same three languages;
cable_takeoff joins SECTION 27. Inventory regenerated; README snapshot 243
tools (238 advertised); registry snapshot analysis: 14.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 12:28:59 +03:00
Umut CANandClaude Opus 5.5 653535bcc1 feat(understand): the takeoff workbook and pipe_takeoff (SECTION 27 opens)
write_workbook builds the user's takeoff workbook from a result: Metraj (rows
with direct, continuity and unassigned lengths split, the allowance as its
own column, routed / unroutable / grand totals), Ozet (by service, diameter
and room), Kontrol (unassigned diameters and unroutable runs with the
reason) and Metodoloji, generated from the result: sources, the length
source and why, the scale-check verdict with its statistics, the units,
every rule and tolerance, and every warning. Headers and sentences in
Turkish, English or Russian. CSV always, one file per sheet; XLSX with
openpyxl, otherwise the xlsx_write refusal naming the office extra while
the CSV files are still written.

pipe_takeoff reads the P&ID and the layout once each (take_snapshot),
computes in a worker thread and never modifies either drawing. Tagged plant
and analysis; the plant pack is the integration task's. Alias record in
English, Turkish and Russian. Inventory regenerated; README snapshot 242
tools (237 advertised); registry snapshot analysis: 13.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 12:22:15 +03:00
Umut CANandClaude Opus 5.5 2b74bf20a6 feat(understand): drawing_topology_check and the three topo_* critique focuses
engineering/understand/topology.py reads a line network (LINE, polylines
with true bulge arcs, ARC) on the chosen layers and reports dangling
ends, near misses (end-end and end-segment within gap, one row per pair)
and interior crossings of two straight segments of one layer. An end on
another network line is joined; an end on any curve of another layer or
inside a block reference's box is attached; a line drawn twice is not a
join, so a doubled pipe still dangles. gap defaults to 0.2 % of the
network diagonal, never under ten tol.

drawing_topology_check joins drawing_diff in SECTION 26: default layers
are those classify_layer calls piping or electrical at >= 0.9, reported
with their classification; no such layer is a refusal, not a pass.

topo_dangling_endpoint (warning), topo_near_miss (error) and
topo_interior_crossing (info) join the closed enum and run under
focus=None, excluding the P&ID and architectural layer sets, so a
pid_from_spec sheet and an arch_plan_from_spec plan get none of them;
a drawing with no candidate layer is never snapshotted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 12:20:24 +03:00
Umut CANandClaude Opus 5.5 7aaead43d9 feat(understand): drawing_scale_check - is this drawing to scale against that one
Equipment tags found exactly once in each drawing are matched (a tag
found twice - a plan copy, a legend - is reported ambiguous and takes no
part; two plan copies resolve to the cluster that matches the most
tags). For every pair of matched tags at least min_pair_mm apart on the
reference, the ratio of their distances is taken in millimetres by each
drawing's inferred unit; the median, the quartiles, the share within
+/-10 % of the median, the same per room of the reference, and the
verdict: to_scale (>= 80 %, with its factor), schematic (<= 50 %),
partly, or insufficient under three matched tags.

server.py grows SECTION 26 to two tools with drawing_scale_check (core,
read-only, tagged analysis/query); alias record, inventory, README
release snapshot 243, frozen group sizes (analysis 14).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 11:55:57 +03:00
Umut CANandClaude Opus 5.5 54325e6de0 feat(understand): drawing_diff - two revisions matched by geometric signature
engineering/understand/diff.py reads each revision once into entity
records and matches them in three stages: exact signature (type, layer,
geometry rounded to tol, text, block, rotation, scale, attributes;
handles ignored), then handle while at least half of the exact matches
kept theirs, then type + layer + nearest anchor within move_tol (default
2 % of the 1st-99th percentile diagonal). The report counts changes by
layer and type, says what changed per entity (moved by, text, attribute,
radius ... from -> to), compares named block definitions by content and
groups the changes into clusters.

drawing_diff opens group D's copy of SECTION 26. It is read-only unless
markup=True, which files revision clouds around the model-space clusters
on the current document through add_revision - refused before any read
with new_path, without a valid rev, or on an engine without revcloud.
A revision path that is neither .dxf nor .dwg is refused naming the
parameter (old_path: / new_path:), like every other path refusal.

Tests: the synthetic plant P&ID edited five ways (a move, a text, an
attribute, an add, a delete) under renumbered handles reports exactly
those five edits; renumbering alone reports nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 11:51:19 +03:00
Umut CANandClaude Opus 5.5 2459694880 feat(understand): drawing_understand - units, extents, clusters, layers, tags, rooms
A drawing somebody else made, read back as one report in which every
claim carries its evidence and a confidence; the drawing is never
modified.

- units.py: $INSUNITS against the unit the geometry implies (text
  heights, wall thickness, door swings, labelled room areas, overall
  size). The header is overruled only when the evidence outweighs it,
  and then with a warning that quotes the numbers. Robust extents grow
  from the quartile box of entity centres; what never joins is an
  outlier, named by handle.
- clusters.py: separate bodies of model-space content by raster,
  8-connectivity and box absorption - two copies of one plan are two
  clusters.
- describe.py: layers with drawn length and the vocabulary's meaning
  (name keyword, else the texts on the layer), equipment tags (one tag
  lettered twice beside its symbol merged, repeats flagged ambiguous),
  room labels in the wall-layer faces of engineering.arch.faces, text
  languages by character class, blocks, layouts, the title block.
- server.py opens SECTION 26 (Understanding & QA) with
  drawing_understand: core, read-only, tagged analysis/query. Alias
  record, inventory, README release snapshot 242, frozen group sizes
  (analysis 13).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 11:50:41 +03:00
Umut CANandClaude Opus 5.5 6e004cc658 feat(arch): arch_plan_from_spec, three arch_* critique focuses, the arch pack, lean 64 and the counts
The three track-F branches are merged; this is the integration that makes the
twelve SECTION 25 tools one surface.

- engineering/arch/spec.py: a whole plan from one document - walls with their
  openings, stairs, room labels measured off the walls just drawn, the
  exterior chains and the schedules - inside one transaction. Everything the
  model, the wall engine or the stair symbol can refuse is refused before the
  transaction opens, by path; a room point in no closed face rolls it back,
  and so does a cancelled request, under the anyio shield of pid/spec.py.
  EXAMPLE_SPEC's rooms measure 4825 x 5750 and 3825 x 5750 - the net floor,
  computed by hand in the tests.
- engineering/arch/critique.py: arch_room_unlabelled, arch_wall_gap (50 mm,
  one issue per pair of walls, measured to the wall body so an L corner and a
  T stem both read as joined) and arch_opening_clash. One index per critique
  run; a drawing without an ACADMCP_ARCH record yields nothing. Each focus
  has a positive and a negative fixture.
- TOOL_PACK_NAMES gains "arch" (the twelve SECTION 25 tools); lean gains
  arch_wall, arch_opening, arch_room and arch_plan_from_spec and reaches 64;
  the arch tag ranks after mech, so SECTION 25 files as one `architecture`
  group - 241 tools in 26 groups.
- Six golden tuning cases, two of them Turkish, the arch_plan_from_spec alias
  record, the shared-command allowlist reconciled with what the merged records
  claim, the inventory regenerated, the registry snapshot retaken, and the
  README figures taken from check_doc_numbers rather than typed.
- Two more Turkish golden cases close the reconciliation's alias gaps
  ("vaziyet planı çiz" -> arch_plan_from_spec, "oda alanlarını hesapla" ->
  arch_rooms_detect); the arch records carry vaziyet / alan / oda / kat planı
  in diacritic and ASCII-folded spellings, because the search tokenizer splits
  on a non-ASCII letter. BOUNDARY and AREA stay with boundary_trace and
  analysis_measure_entity on purpose, recorded in the arch alias comment.
  Tuning top-1 73/84, top-3 83/84; holdout top-3 15/15.
- tests/test_token_suite.py: TOOL_PACKS=core,mech now drops the arch pack
  too (measured 193 = 236 - 9 - 22 - 12).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 23:20:05 +03:00
Umut CANandClaude Opus 5.5 4c5c8227ef merge: track F group C
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 23:05:27 +03:00
Umut CANandClaude Opus 5.5 18db0b46c0 merge: track F group R
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 23:03:13 +03:00
Umut CANandClaude Opus 5.5 cd1ae3529c feat(arch): SECTION 25 opens - walls, openings, stairs and exterior chains as tools
engineering/arch/draw.py is the only module of the track that touches a
backend. It draws through draw_prims(role_layer=ARCH_ROLE_LAYER) and the
generic contract members, so it runs on both engines as written, and it writes
the plan onto the drawing: each element's ACADMCP_ARCH record on one of its own
entities (a wall's first face line, a door's leaf, a window's frame, a stair's
first riser) and, beside it under ACADMCP_ARCH_H, the handles that element
owns. read_plan reads the records back and never modifies the drawing.

arch_wall computes the whole network before and after the new walls and
redraws only the drawn walls whose outline changes - erasing exactly the
entities they own - and says which under redrawn. arch_opening hosts a door or
a window in a drawn wall found by id and redraws that wall cut; its tag
defaults to the next D1/W1 (K1/P1 in Turkish). A new wall that would leave a
drawn opening uncuttable, and an opening that runs into a junction, are refused
before anything is erased. arch_stair reports the Blondel value; it does not
enforce it.

exterior_chains gives each side up to three rows, nearest first: every jamb in
that facade, both faces of every wall meeting it, and the overall - from the
wall engine's own faces. A row that would only restate a farther one is not
emitted (ISO 129-1: each measurement once). The 10 mm / 8 mm row spacing is a
declared drafting default times the plot scale, not a standard value.

The arch tag is ranked after mech so the four tools file under a new
architecture group; alias records carry WALLADD/MLINE, DOORADD/WINDOWADD and
STAIRADD with English and Turkish shop words. Inventory regenerated; README
snapshot 233 tools (228 advertised) in 26 groups; registry snapshot
architecture: 4.

No new ActiveX member is called. Measured on AutoCAD 2026 in a scratch
document through ComBackend: shell + teed partition, D1 + W1, a straight 17-riser stair
and the bottom chains gave the same 20 wall LINEs (identical endpoints),
9 poche HATCHes, 1+1 door, 4 window lines, 11 dimensions and a read_plan
round trip as the headless engine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 23:00:05 +03:00
Umut CANandClaude Opus 5.5 9168d61397 feat(task-6): door, window and room schedules read from the drawing
engineering/arch/schedule.py turns the plan records into schedule rows: doors
by tag with width, height, swing, hand and wall; windows by tag with width,
height, sill and wall; rooms by number with name and the measured area the
label carries, plus a total. Tags compare as numbers (D2 before D10). An
opening without a tag gets the next free D1 / W1 - K1 / P1 in Turkish - in
wall order; an explicit tag is never renumbered and a generated one never
repeats it. A value the model does not carry is an empty cell, never a default,
and Turkish output writes decimal commas.

draw_schedule reads the ACADMCP_ARCH records off the drawing through the one
reader rooms.py already has and draws a real TABLE the way the ISO 7573 parts
list does - top-left insertion, the table growing down, the parts list's 7 mm
rows and 2.5 mm text at the plot scale, the engine's representation reported.
Nothing to schedule, an unknown kind or language and more rows than one TABLE
takes are refused before anything is drawn.

arch_schedule joins arch_room and arch_rooms_detect under SECTION 25's rooms
marker (banner 3 tools), shares the TABLE command with entity_create_table and
bom_table, and the counts follow: 232 tools in 25 groups.

Live fix in com_backend.entity_create_table. MEASURED on AutoCAD 2026: IAcadTable
has no TextHeight property. pywin32 raised AttributeError on
`table.TextHeight = h`, the old code swallowed it, and every live table kept
the Standard style's heights (title 6.0, header and data 4.5), which is 0.1 mm
on paper at 1:50. The member that works is SetCellTextHeight(row, col, h),
probed on a scratch document: every cell read back 125.0 via
GetCellTextHeight. The row heights stayed 350.0 and GetBoundingBox matched the
reported bbox (10000,3250)-(15750,5000). Then draw_schedule ran end to end
through ComBackend on a scratch document: a native TABLE on A-SYMB-T-N for
doors (5 rows) and rooms/tr (6 rows, total 45,00 m²), and the document was
closed afterwards. The new fake in tests/test_table_leader.py has only the
measured members.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 22:56:05 +03:00
Umut CANandClaude Opus 5.5 f5f11178a9 feat(task-8): SECTION 25 grid and symbols - arch_grid, arch_symbol
engineering/arch/symbols.py is pure: grid_prims() draws each structural axis on
the grid role - its CENTER linetype comes from the A-GRID-E-N row, no primitive
sets one - with a 5 mm (paper) bubble and its label touching both ends, on the
symbol role so the circle does not print dashed. Numbers run along x and
letters along y (A..Z, AA, ... none skipped) unless labels names them;
positions are sorted, duplicates and label-count mismatches refused by path.

symbol_prims() draws the four plan symbols: the north arrow (a circle, an
arrow turned CCW from +Y, the vocab(lang) north letter upright beyond the tip),
the section mark (the cut line, a labelled bubble past each end and a viewing
arrow at each end, p1/p2 relative to the origin), the level mark (the triangle,
its line and the value formatted +3.00 / ±0.00, with a decimal comma in
Turkish) and the elevation mark (a circle whose pointers are the tangents from
r*sqrt(2) touching it at +/-45 degrees). An unknown kind is refused with the
list, a parameter the kind does not take with the ones it does. Every size is
a declared paper-mm drafting choice times the plot scale, not a standards value.

arch_grid and arch_symbol draw through draw_prims with ARCH_ROLE_LAYER; alias
records (COLUMNGRIDADD, English and Turkish synonyms), a regenerated inventory,
the README snapshot at 233 tools and the branch-local group snapshot
(entity_creation 21) complete the registration.

Beyond the plan: grid_axes refuses a labels value that is not a dict and a
labels.x / labels.y that is not a list (a string was split into one label per
character, a number raised a bare TypeError). No new ActiveX member: a live run
on AutoCAD 2026 (scratch document, closed after) drew the grid and all four
symbols through draw_prims - A-GRID-E-N created with CENTER, bubbles, texts and
symbols on A-SYMB-T-N, the Turkish north letter K and the level "+3,00".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 22:42:42 +03:00
Umut CANandClaude Opus 5.5 afd42356b5 feat(task-7): SECTION 25 catalogue - arch_catalogue_list, arch_catalogue_insert
engineering/arch/catalogue.py is the standard-parts pattern of track B for an
architectural plan: twenty authored plan symbols in two families - thirteen
furniture items (single and double bed, wardrobe, three-seat sofa, armchair,
chair, dining table with four chairs, desk, kitchen counter run, fridge,
bookshelf, nightstand, coffee table) and seven sanitary items (WC, bidet, wall
basin, shower tray, bathtub, kitchen sink, washing machine). Every size is a
nominal catalogue dimension and says so in SIZE_BASIS, in each row and in both
tool docstrings; no standard is cited because none fixes these sizes.

Each item is typed block_define primitives on layer 0 inside [0, w] x [0, d],
origin at the back-left corner with the wall side along +X - a test holds all
twenty to their footprint. catalogue() searches name, family and the English
and Turkish labels, ignoring case and Turkish diacritics (buzdolabi finds
buzdolabı). insert_item() defines ARCH_<NAME> once, creates the furniture or
sanitary layer from its ARCH_LAYERS row when missing (live ActiveX refuses a
missing layer), inserts with an invisible ITEM attribute, and refuses an
unknown name with the nearest catalogue names before any write.

SECTION 25 opens on this branch with the two tools, alias records (INSERT,
ADCENTER, TOOLPALETTES and English/Turkish synonyms), a regenerated inventory,
the README snapshot at 231 tools, and the branch-local group snapshot
(entity_query 10, entity_creation 19) that the track F merge recomputes.

No new ActiveX call: insert_item composes the existing layer_list,
layer_create, block_list, block_define and block_insert members. Run once live
through ComBackend on AutoCAD 2026 (scratch document, closed after): all
twenty items defined and inserted, a second double_bed insert reported
defined=false, ITEM read back as double_bed, 20 ARCH_* blocks, 21 INSERTs on
A-FURN-E-N / A-SANR-E-N, both layers created at ACI 5 / 0.18 mm.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 22:35:44 +03:00
Umut CANandClaude Opus 5.5 67d578e09a feat(task-5): planar faces, measured room labels and the foreign-plan reader
engineering/arch/faces.py finds the closed faces a set of straight segments
bounds: endpoints within tol merge, every segment splits where another crosses
or touches it (the T-junction a partition makes), dangling edges are pruned,
and the half-edge walk turns to the next edge clockwise so every bounded face
comes out counter-clockwise. A piece nested inside a face of another piece is
a hole in it, so a ring of walls measures its body and a room with a column
measures its floor. Areas come from engineering.measure.polygon_area_perimeter,
the arithmetic analysis_measure_entity reports.

engineering/arch/rooms.py labels a room with the area of the face its point
lies in - a 4000 x 5000 room between 200 mm walls reads 20.00 m2, never the
axis area - and writes the room record onto the name text. Opening records
close door and window gaps along the wall faces, so two rooms either side of a
door stay two rooms. rooms_detect reads any plan's lines, drops faces under
min_area and thin wall bodies (mean width under 600 mm, a declared heuristic),
reports confidence 1.0 for faces enclosed only by our wall layer and 0.6
otherwise, lists arcs as skipped rather than flattening them, and never
modifies the drawing.

Two hardenings beyond the plan text, each with a test: read_records pages the
current space once and filters to the arch layers locally (the plan paged it
once per arch layer - twelve full scans, each a cross-process call per entity
on a live seat), keeping the same layer-then-drawing order; and an opening
record whose offset falls outside its wall (a stale record) is listed in
omitted instead of raising out of the read-only detector.

No new ActiveX member is called: both tools compose entity_list,
entity_get/set_xdata, layer_list and draw_prims, whose COM paths are already
live-verified.

SECTION 25 opens on this branch with arch_room and arch_rooms_detect under
the rooms marker; until Task 9 gives arch its group they file under
entity_creation and entity_query. Alias records, a regenerated inventory, the
README snapshot at 231 tools in 25 groups and the frozen group-size snapshot
follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 22:33:37 +03:00
Umut CANandClaude Opus 5.5 3b033033f9 feat(mech): six mech_* critique focuses, the mech pack, lean 60, aliases and the counts
The four track-B/G branches are merged; this is the integration that makes the
25 new tools one surface instead of four.

- engineering/mech/critique.py: the six focuses of spec 13, dispatched the way
  the P&ID ones are. One index per critique run, shared through run_critique's
  context, so six focuses cost one read of the drawing; a drawing with no
  ACADMCP_MECH payload yields nothing at all, which is what keeps focus=None
  cheap for a P&ID sheet or a plain DXF. Each focus has a positive and a
  negative fixture.
  Two narrowings are in the module docstring rather than hidden:
  mech_view_misaligned checks the projection axis and not the quadrant,
  because the pinned payload carries no projection angle to check it against,
  and mech_duplicate_dimension catches two dimensions in the same place rather
  than a semantic restatement, because no engine exposes what a dimension
  measured.
- The index reads what the tools actually write, not only the plan's fixture
  shape. mech_part_draw anchors its part payload on a POINT (layer MECH) and
  lists every drawn view under "views"; the plan's index read INSERT/CIRCLE
  only and one "view" per payload, so it was silent on every part the server
  draws. It now reads POINT too, expands "views" into one entry per drawn view
  (the section check looks in the section's own WCS box, the alignment check
  compares frame_bbox centres), and pages entity_list instead of one call.
  Test: a part drawn by draw_part + add_view is read and passes all six; the
  same part with its hatch erased is reported.
- Live type names, measured on AutoCAD 2026 (2026-09-23, scratch document,
  closed): ObjectName AcDbBlockReference -> BLOCKREFERENCE, AcDbRotatedDimension
  -> ROTATEDDIMENSION, AcDbAlignedDimension -> ALIGNEDDIMENSION, AcDbPoint ->
  POINT with GetBoundingBox = the point. The index reads BLOCKREFERENCE as an
  INSERT and any *DIMENSION as a dimension; a wrapper test reports the ezdxf
  entities under exactly those measured names.
- mech_thread_unrepresented uses the view engine's own MINOR_RATIO, and checks
  the thin line ISO 6410 draws: the minor diameter of an external thread, the
  major of an internal one.
- mech_bom_balloon_mismatch: identical parts share one balloon (ISO 6433
  balloons a parts-list row), so a second M12x60 is covered by the first one's
  balloon instead of reported.
- TOOL_PACK_NAMES has "mech" (the 14 tools of SECTIONS 21-23). SECTION 24
  stays core: a frame, a title block, a parts list and an xref are universal
  drafting, not a vertical. Lean gains five and reaches 60 (54 with
  TOOL_PACKS=core).
- _GROUP_TAG_PRIORITY gains sheet directly after mech; the mech group label
  becomes "mechanical". SECTION 24's last four tools (xref_attach,
  xref_manage, image_attach, drawing_export_dwg) now carry the sheet tag, so
  group, pack and SECTION are one list: 229 tools in 25 groups, and
  engineering/blocks/entity_creation/drawing return to their pre-SECTION-24
  sizes.
- Ten golden tuning cases, two of them Turkish (with the diacritic spellings
  added to the alias records next to the ASCII-folded ones). The shared
  AutoCAD command set needed no change: the merge had already resolved TABLE,
  REVCLOUD, SECTIONPLANE, INSERT, ARRAY and SAVEAS, and nothing is stale.
- Inventory regenerated, the frozen group-size snapshot retaken from the live
  registry, and the README release-snapshot / pack / lean figures taken from
  scripts/check_doc_numbers.py rather than typed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 12:48:44 +03:00
Umut CANandClaude Opus 5.5 a6c9c0d927 merge: tracks B+G group G
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 12:25:45 +03:00
Umut CANandClaude Opus 5.5 76e8647e1b merge: tracks B+G group A
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 12:25:44 +03:00
Umut CANandClaude Opus 5.5 36dab4cec6 merge: tracks B+G group S
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 12:25:44 +03:00
Umut CANandClaude Opus 5 8b14d4ef8d feat(task-7): SECTION 21 - the mechanical part model as six tools
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
2026-09-22 23:17:29 +03:00
Umut CANandClaude Opus 5 3ef316d520 feat(refs): xrefs, raster underlays, data extract and real DWG output
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
2026-09-22 23:03:57 +03:00
Umut CANandClaude Opus 5 499b642fbd feat(sheet): revisions, the ISO 7573 parts list and ISO 6433 balloons
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
2026-09-22 20:48:50 +03:00
Umut CANandClaude Opus 5 84c1ef42fb feat(task-9): SECTION 22 - std_part_list, std_part_insert, std_feature_draw
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
2026-09-22 19:26:49 +03:00
Umut CANandClaude Opus 5 64a57d74a0 feat(mech): SECTION 23 completes with centre_marks, section_line, hatch_material
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
2026-09-22 19:25:13 +03:00
Umut CANandClaude Opus 5 56e6ff7206 feat(sheet): ISO 5457 frames and ISO 7200 title blocks for A4-A0
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
2026-09-22 19:03:14 +03:00
Umut CANandClaude Opus 5 a4ce46ba9b feat(mech): SECTION 23 opens with surface_texture and weld_symbol
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
2026-09-22 18:55:57 +03:00
Umut CANandClaude Opus 5 70afd57831 feat(server): settings pack — 22 environment tools behind TOOL_PACKS, lean grows to 55, three groups, golden set +10
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
2026-09-22 11:35:00 +03:00
Umut CANandClaude Opus 5 90c1194f7f merge: track-e group V
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 11:18:03 +03:00
Umut CANandClaude Opus 5 8e05b5e8eb merge: track-e group P
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 11:14:02 +03:00
Umut CANandClaude Opus 5 3278586c6e merge: track-e group C
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 11:12:15 +03:00
Umut CANandClaude Opus 5 a5d166f365 feat(styles): drawing_apply_standard — ISO or ANSI styles, units and layers in one idempotent call
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-21 22:49:12 +03:00
Umut CANandClaude Opus 5 b458923e19 feat(templates): drawing_template_save on both engines — dwt_write is a declared capability
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
2026-09-21 22:48:31 +03:00
Umut CANandClaude Opus 5 a76d9e08a3 feat(templates): five bundled templates built by the server's own tools; drawing_new(template=name)
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
2026-09-21 22:03:41 +03:00
Umut CANandClaude Opus 5 a60f801205 feat(server): SECTION 18 styles — dimstyle/textstyle/mleaderstyle tools with DIMSTYLE/STYLE/MLEADERSTYLE aliases
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-21 21:44:02 +03:00
Umut CANandClaude Opus 5 f25421709f feat(pagesetup): SECTION 19 — page_setup_list/apply, plot_style_list, batch_plot with /MediaBox read-back
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-21 18:39:34 +03:00
Umut CANandClaude Opus 5 b83a74e969 feat(environment): system_launch, preference whitelist and operator prompts on COM; declared refusals headlessly
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
2026-09-21 18:34:09 +03:00
Umut CANandClaude Opus 5 10a7372ce7 feat(settings): drawing_properties_get/set — SummaryInfo on COM, custom properties on both engines
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
2026-09-21 16:59:19 +03:00
Umut CANandClaude Opus 5 125a8ed3b8 feat(settings): SYSVAR_CATALOG + system_variable_describe; system_set_variable refuses out-of-range values
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
2026-09-17 14:56:25 +03:00
Umut CANandClaude Opus 5 dcc8f07bf4 feat(environment): portable layer states, named views and UCS on both engines
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
2026-09-17 09:47:39 +03:00
Umut CANandClaude Opus 5 174fb8010c feat(environment): multi-document on both engines — ezdxf document registry, document_list/activate/close, drawing_close folded in
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
2026-09-16 21:20:21 +03:00
Umut CANandClaude Opus 5 c441609254 feat(pid): pid_tag_parse, catalogue and ISA-5.1 resources, prompt rewritten onto the tools
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-16 12:29:04 +03:00
Umut CANandClaude Opus 5 d80ae17411 feat(pid): pid_from_spec — a whole P&ID in one transaction, graph and critique returned
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-16 12:10:14 +03:00
Umut CANandClaude Opus 5 4e523ac6ed feat(pid): instrument index, line list and equipment list from the graph
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-16 10:49:30 +03:00
Umut CANandClaude Opus 5 e246b5e86a feat(pid): pid_graph reads the drawing back with ports, junctions, dangling ends and confidence
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-16 00:13:42 +03:00
Umut CANandClaude Opus 5 e8f1a666f8 feat(pid): pid_line_draw — port-to-port orthogonal lines with class, number, markers
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-15 23:26:09 +03:00
Umut CANandClaude Opus 5 4b6abf7b92 feat(pid): pid_symbol_list and pid_symbol_insert — blocks with tags, ports and XDATA
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-15 16:44:06 +03:00
Umut CANandClaude Opus 5 cda8b69dba feat(entity): entity_get_xdata / entity_set_xdata on both engines
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-15 14:16:12 +03:00
Umut CANandClaude Opus 5 a84cae2641 feat(blocks): block_define — typed primitives + ATTDEFs on both engines
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-15 13:58:09 +03:00
Umut CANandClaude Opus 5 25f8737132 release: v1.5.1 — bind guard failed open, [full] would not install, DIMSCALE did nothing
Three defects found after 1.5.0 reached PyPI, two of them by the adversarial
pass over the rewritten README.

The HTTP bind guard failed open on the launch path it was written for.
`_guarded_run_async` read the bind address from the `host` kwarg and treated an
absent one as loopback. It is not: `fastmcp run` forwards that kwarg only when
`--host` was passed, and fastmcp otherwise resolves the host from
`fastmcp.settings.host`, which is environment-backed. So

    FASTMCP_HOST=0.0.0.0 fastmcp run server.py:mcp --transport http

bound every interface -- 149 tools including file open/save and AutoCAD command
execution, no token -- while the guard inspected the literal "127.0.0.1" and
waved it through. v1.4.0's changelog claimed this guard "fires on every launch
path"; it fired, and looked at the wrong host. It now resolves the effective
host, and warns rather than staying silent if it cannot.

`[com]` and `[full]` could not be installed off Windows. Both listed
`pywin32>=306` with no environment marker, and pywin32 publishes Windows wheels
and no sdist, so `pip install autocad-mcp-pro[full]` died at resolution on
Linux and macOS -- and `[full]` is the extra the README recommended to
contributors. CI never caught it because every Linux lane installs `[pdf]` or
syncs from uv.lock, whose resolution markers hide the gap. Both entries carry
`; sys_platform == "win32"` now, with a release-consistency test to keep them.

`drawing_settings({"dimscale": ...})` reported success and changed nothing
headlessly. It writes `$DIMSCALE`, which is what AutoCAD reads when it draws a
dimension, so the call worked on COM; ezdxf renders from the dimstyle table
entry and never consults the header. One call, one reported success, two
different drawings. The header value now folds into the per-dimension override
on all five dimension tools -- including `dimension_aligned` and
`dimension_angular`, which do not share the tolerance-override path and the
obvious fix would have missed. DIMSCALE 1.0 writes nothing.

README: every engine-conditional family in the feature table now says which
engine (boundary tracing, WIPEOUT/REVCLOUD/MTEXT masks, CHSPACE, typed hatch
edge paths, headless undo), and the checkout install line stopped recommending
an extra that could not resolve on the reader's platform.

1257 passed, 1 xfailed. 1.5.0 is to be yanked: its diameter and radius callouts
put wrong numbers on drawings at default settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 14:28:35 +03:00