Files
autocad-mcp-pro/docs/superpowers/specs/2026-09-22-v1.6-mech-design.md
T
Umut CANandClaude Opus 5.5 898e775f93 bench: matrix v5 adds mech_assembly; the correctness suite grows with the mechanical and sheet checks
The evidence for tracks B and G, in the shape tracks A and E used.

- benchmarks/tasks_v5.py: 18 tasks, DEFAULT_MATRIX = "v5"; v4, v3 and v2 stay
  addressable so an earlier report reproduces exactly rather than being only
  described. mech_assembly is roadmap criterion 3 - a flange-coupling sheet
  built from the part model and the sheet standard, through the code the
  tools call (apply_titleblock, draw_part/add_view/dimension_part,
  insert_std_part, extract_records -> draw_bom_table, add_balloon), gated on
  two numbers that cannot be negotiated: drawing_critique(focus=None) returns
  zero issues and the finalize score is at least 90 (it scores 100, 18/18 on
  the v5 matrix). The adapter returns the focuses that fired, so a
  regression names itself, and a test drops one balloon to prove the gate
  fails and names mech_bom_balloon_mismatch.
- Seven correctness checks (32 -> 39). mech_section_hatch_area is the one
  that cannot be faked: a sleeve of length 60, outside diameter 40 and bore
  20 cuts two faces totalling 1200 mm^2 exactly, read back out of the drawing
  through entity_measure. sheet_frame_iso5457_a3 pins the A3 frame (20,10 to
  410,287 inside the 420 x 297 trimmed sheet); mech_iso286_on_dimension pins
  40 H7 at +0.025/0 through the layout; std_part_iso4014_m12 pins s = 18 and
  k = 7.5 by value; mech_thread_unrepresented_is_caught is the seventh: a
  quality gate that never fires is not a gate.
- scripts/smoke_mech_com.py executed every new COM path once on AutoCAD 2026
  (25.1s): frame, title block, a sectioned shaft with 12 dimensions, an ISO
  4014 block with 1 ACADMCP_MECH chunk, a weld symbol, the parts list read off
  the drawing, a linked balloon, an xref attached / listed / detached, a
  31687-byte .dwg (AC1032) through ActiveX SaveAs, critique 0 issues, exit 0,
  every scratch document closed. It retries RPC_E_CALL_REJECTED (measured:
  the first call after Documents.Add is refused) and tracks its documents by
  the document list, since SaveAs renames them.
- Defects the evidence found, each fixed with a test that fails on the old
  code:
  * ezdxf dimension_diameter put the text beyond the SECOND chord point;
    ActiveX AddDimDiametric puts it beyond the first (measured live: a d50
    from (x,170) to (x,220), leader 10, text at (x,160)). Headlessly every
    front-view diameter stood on its own chain dimension. ezdxf now follows
    ActiveX.
  * draw_part's anchor goes on MECH; ActiveX refuses entity.Layer on a
    missing layer (measured -0x7ffdfff7 'Key not found'). draw.py ensures
    every target layer before the first entity.
  * bom.extract_records filtered on INSERT; ActiveX names it BLOCKREFERENCE
    (ObjectName AcDbBlockReference) - the live parts list was empty.
  * COM _entity_info reported no dimension text_position: ModelSpace.Item
    narrows to IAcadEntity, which has no TextPosition (measured
    AttributeError); read through _com_unnarrow (TextPosition measured on
    AcDbDiametricDimension, WCS). dim_overlap compared bbox centres and a
    diameter collided with its own bore.
  * pid graph raised KeyError 'start' mid-critique on a LINE whose point read
    AutoCAD refused while busy; such a line is left out.
  * the validator read "ISO 4014 - M12x60" as fake dimension text.
- The published A/B report is re-run over the grown suite rather than
  re-typed (26/39 -> 39/39, 13 fixed, 0 regressed), check_doc_numbers derives
  the task count from the v5 matrix, the token table is remeasured with a
  TOOL_PACKS=core,mech row (193 tools / 61,673 tokens), and the README's
  collected-tests figure is retaken (3990).

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

28 KiB
Raw Blame History

v1.6 Tracks B + G — the complete mechanical 2D part drawer and the sheet standard

Date: 2026-09-22 · Status: shipped (v1.6.0-dev, tracks B + G) · Parent: 2026-09-15-v1.6-roadmap-design.md (tracks B and G, merged into one spec by the user's decision) · Baseline: main after Track E (39e6e2f, 204 tools, 8 resources, 3283 tests)

1. Summary

v1.5 can draw a gear, a keyed bore and an A3 title block. Everything else a machine drawing needs — a stepped shaft, a plate with holes, a flange, a section view, a hidden line, a surface-texture symbol, a weld symbol, a parts list, a sheet frame that is not A3 — a model has to hand-draw from primitives. That is the failure this track closes, and the repository already knows why it must: CLAUDE.md forbids hand-drawing teeth, keyways and section hatches because a model inventing an involute flank or a DIN 509 undercut from memory produces geometry that looks like a drawing and is wrong.

The design is not a zoo of per-part generators. It is one part model — a profile plus a list of typed features — and one view engine that turns that model into front / side / top / section / detail views with correct hidden lines, cut faces, hatch and dimensions. The model decides what the part is; the tools guarantee what the standards say. A "flanged shaft with a keyway and a bolt circle" needs no new code: it is a revolved profile with three features.

Around it: a standard-parts catalogue authored as code (fasteners, bearings, retaining rings, undercuts, centre holes, O-ring grooves) inserted as real blocks with attributes — the Track A pattern, which gives the parts list for free; the ISO annotation symbols; and the sheet standard (ISO 5457 frames A4–A0, ISO 7200 title blocks for every size, revisions, parts list, balloons, xrefs, images, DWG output).

Tool budget: +25 → 229. New pack mech (sections 21–23, 14 tools); the sheet and delivery section is core — a frame, a title block, a parts list and an xref are universal drafting, not a vertical.

The user's principle for this track (2026-09-16, restated 2026-09-21): tool count is not the goal. Where several operations act on the same object they are one tool with an enum (mech_view_add(kind=…), xref_manage(action=…)), never three thin ones; every tool carries a rich alias record (AutoCAD command names plus shop-floor Turkish/English synonyms), a docstring that names its refusals, a pack, and a lean decision.

2. Scope

In: sections 4–10 of this document, registered as SECTION 21 Mechanical parts (6), SECTION 22 Standard parts (3), SECTION 23 Mechanical annotation (5), SECTION 24 Sheet and delivery (11).

Out of 1.6 (recorded, with the reason):

  • A general 3D → 2D projection kernel. Views come from the part model, which is 2.5D by construction (revolved or prismatic). An arbitrary ACIS solid has no view path and is not pretended to have one.
  • Sheet-metal flat patterns and bend allowance (K-factor). Bend lines are drawn as a representation; no developed length is computed, because a wrong developed length is a scrapped part.
  • Splines (DIN 5480 / ISO 4156), springs (DIN 2089), gear tooth profiles other than the existing involute spur/helical generator.
  • Assembly constraint solving. mech_part_from_spec places parts at given coordinates; it does not mate them.
  • Third-angle projection as the default — it is available, but the default is first angle (ISO 128-30, European practice, which is what this shop draws).
  • Real DWG output from the headless engine unless the ODA File Converter is installed (the user chose this: COM writes DWG natively, ezdxf refuses with the dwg_write capability and names the ODA route).
  • XLSX export unless openpyxl is present (the [pdf]-extra pattern); CSV is always available.
  • Clipping a raster underlay (image_attach places and scales; a clip boundary is a 1.7 item), and placing a title block anywhere but the ISO 7200 corner the standard fixes.
  • Four feature tables ship empty (recorded 2026-09-22, during planning): DIN 509 E/F, DIN 471 / DIN 472, DIN 332-1 A/B and ISO 3601-2. Their structure, SOURCE, row-key contract and coverage refusal ship; no row does, because no row could be verified against the standard's own table on this machine (the repository's web tool is out of credits). The features draw correctly from explicit dimensions — Undercut(profile=…), RetainingGroove(m=, d2=), CentreHole(d1=, d2=, d3=, t=), ORingGroove(b=, h=) — and every lookup is refused by name until a row is transcribed. Filling a table later is a data edit with no code change, and a shape test already fails any row that lands missing a column the drawing code reads. The alternative — plausible numbers — ships a wrong workshop drawing, which is the one failure this repository refuses to risk.

3. Architecture

engineering/mech/
  __init__.py          public surface: build_part, draw_part, add_view, …
  part.py              the part model: RevolvedPart, PrismaticPart, Feature protocol
  features.py          every feature dataclass + its standard lookup + validation
  views.py             the view engine: silhouette, hidden lines, section, detail
  dimension.py         dimension intents per feature → laid-out DIMENSION calls
  draw.py              async draw_*(backend, …) — the only module that touches a backend
  primitives.py        the typed primitive descriptions the pure modules emit
                       (Line, Arc, Circle, Poly, HatchArea, DimIntent, Role → layer)
  xdata.py             ACADMCP_MECH payload: the part model written onto the drawing
  critique.py          the six mech_* critique focuses
  standards/
    __init__.py        registry + `lookup(standard, size)` with coverage refusal
    fasteners.py       ISO 4014, ISO 4017, ISO 4032, ISO 7089, ISO 4762
    bearings.py        ISO 15 series 60xx/62xx/63xx + ISO 8826-1 representation
    rings.py           DIN 471 / DIN 472 groove dimensions
    undercuts.py       DIN 509 forms E and F
    centres.py         DIN 332 forms A, B, R
    orings.py          ISO 3601-2 groove dimensions
    threads.py         ISO 261 / ISO 965 coarse + fine pitch, ISO 6410 representation
    materials.py       ISO 128-50 material → hatch pattern/angle/scale map
engineering/sheet/
  __init__.py
  frames.py            ISO 5457 sheet sizes, margins, zone grid, centring/trim marks
  titleblock.py        ISO 7200 title block for A4–A0 (today's A3 module folds in here)
  revision.py          revision block rows + revcloud + triangle tags
  bom.py               ISO 7573 parts list: extract → rows → table; balloons ISO 6433
  extract.py           attributes/XDATA → CSV (always) / XLSX (optional dependency)

backends/contracts/refs.py    (new)  xref_attach, xref_manage, image_attach, drawing_export_dwg
server.py  SECTION 21 (6) · SECTION 22 (3) · SECTION 23 (5) · SECTION 24 (11)
scripts/smoke_mech_com.py     live COM execution of every new COM path, once

Only four new contract members. Corrected while planning, 2026-09-22: the existing engineering tools (gear_draw_*, keyway_draw_*, titleblock_apply_iso_a3) are not contract members — they take the backend and compose the generic ones (entity_create_*, dimension_*, block_define, entity_set_xdata). Every mechanical, annotation, parts-list and frame tool follows that pattern, so it is written once and works on both engines by construction. Only the four operations that genuinely differ per engine become contract members with an implementation each: xref_attach, xref_manage, image_attach, drawing_export_dwg. This removes two contract files, halves the backend work, and keeps the capability map to the three keys of §12.

The split that matters: part.py, features.py, views.py, dimension.py and everything under standards/ are pure — no backend, no async, no DXF. They take numbers and return primitive descriptions (typed tuples: lines, arcs, circles, polylines, hatch boundaries, dimension intents, each with a layer and a line role). draw.py is the only module that turns those descriptions into backend calls. This is what makes the view engine testable without a drawing: a test asserts the emitted primitive list, not a screenshot.

Both engines implement every contract member. The COM paths are fake-tested and executed live once by scripts/smoke_mech_com.py on AutoCAD 2026.

4. The part model

class Segment(NamedTuple):
    length: float
    d_outer: float
    d_inner: float = 0.0               # 0 = solid; > 0 = a bore through this segment
    taper_to: float | None = None      # cone: outer diameter at the far end

@dataclass(frozen=True)
class RevolvedPart:
    name: str
    segments: tuple[Segment, ...]      # left to right along +X
    features: tuple[Feature, ...] = ()
    material: str = "steel"            # drives the ISO 128-50 hatch

@dataclass(frozen=True)
class PrismaticPart:
    name: str
    outline: tuple[tuple[float, float], ...]   # closed polygon, CCW, part-local XY
    thickness: float                            # along +Z
    features: tuple[Feature, ...] = ()
    material: str = "steel"

Rules the model enforces before any write (the repository's validate-first rule):

  • every number finite and positive where positivity is physical (a segment length, a diameter, a thickness); a non-finite value is refused by path (segments[2].d_outer), the way pid_from_spec refuses;
  • d_inner < d_outer per segment; a bore that changes diameter is two segments;
  • the outline is closed, non-self-intersecting (reuse engineering/measure.py's is_self_intersecting), and has ≥ 3 distinct vertices;
  • every feature's placement lies on the part: a keyway on segment 5 of a three-segment shaft is refused by name, not clamped;
  • overlapping features that would remove the same material twice (two grooves at the same x, a hole inside a pocket's wall) are refused with both names.

The part is written onto the drawing as ACADMCP_MECH XDATA on the view's anchor block (the Track A chunking codec, ≤ 255 chars per chunk, 16 KB per entity), so mech_part_inspect reads the model back and mech_view_add can add a second view months later without the caller re-describing the part.

5. Feature catalogue

Every feature is a frozen dataclass that answers three questions: where am I on the part, what geometry do I contribute in a given view (visible / hidden / cut), and what dimension do I want. Features applicable to revolved parts:

Feature Parameters Standard Coverage
Chamfer at (start/end/step:i), size, angle=45 ISO 128-3 any
Fillet at step:i, radius — any
Keyway segment, length, from_end, width/depth or derived DIN 6885-1 d 6–130 mm (existing keyway.py table)
Thread segment, designation (M20x1.5), length, external/internal, hand ISO 261/965 pitch, ISO 6410 representation M1.6–M64 coarse, M8–M64 fine
Undercut at step:i, form E/F DIN 509 d 18–80 mm
RetainingGroove x, shaft or bore, nominal d DIN 471 / DIN 472 d 8–100 mm
CentreHole at start/end, form A/B/R, size DIN 332-1/2 d 1–10 mm
ORingGroove x, cord diameter, static radial/axial ISO 3601-2 cord 1.78–8.40 mm
CrossHole x, diameter, angle, through/blind depth — any
HoleCircle x (face), pcd, count, diameter, start angle, thread? — any
AxialBore from end, diameter, depth (or through) — any
GearTeeth module, teeth, pressure/helix angle existing gear.py as today

Applicable to prismatic parts:

Feature Parameters Standard Coverage
Hole x, y, diameter, through/depth, counterbore/countersink/thread ISO 6410 for tapped any
Slot x, y, length, width, angle — any
Pocket outline, depth — any
CornerFillet / CornerChamfer vertex index, r / size — any
BendLine p1, p2, angle, inside radius representation only any
HolePattern kind linear/polar/grid, base feature, counts, spacings — expands to Holes before drawing

mech_hole_pattern is the same expansion exposed as a tool, for holes that are not part of a modelled part: it takes a base hole and a pattern and draws the circles, centre marks and (optionally) the dimensions, on any drawing.

The table rule, non-negotiable: every row of every table under standards/ is transcribed from the named standard's own table, carries a SOURCE docstring naming the standard, edition and table, and is pinned by a provenance test that asserts named values. A size outside the transcribed range is refused by name (ValueError: DIN 509 form E covers d 18–80 mm; 95 mm is outside the table) — never interpolated, never "approximately". If the implementer cannot verify a row against the standard, the row is not shipped and the coverage note says so. This is the same discipline engineering/fits.py (ISO 286) already follows, and it is what keeps a fabricated number out of a workshop drawing.

6. The view engine

A view is a projection of the part model onto a plane, plus the features that land on it. The engine emits primitive descriptions on ISO 128 line roles, which map to the engineering layers the server already bootstraps:

Role Layer Line
visible outline GEOMETRY continuous, 0.50 mm
hidden edge HIDDEN HIDDEN, 0.25 mm
centre line / axis CENTER CENTER, 0.25 mm
section cut face HATCH pattern per material, 0.25 mm
dimension DIM 0.25 mm

Revolved parts. front is the axial view: the profile silhouette and its mirror about the axis, with the axis on CENTER. Steps produce vertical edges; chamfers and fillets replace the corner; a Thread draws the ISO 6410 representation (major continuous, minor thin at 0.8·d, runout at 30°); a Keyway shows its two visible edges plus the hidden depth line; a CrossHole appears as two hidden lines; grooves cut a notch in the silhouette. side (the end view, left or right) is the set of concentric circles the profile crosses at that end, plus hole circles, plus the keyway notch, with centre marks.

Prismatic parts. front is the outline with features drawn in true shape (holes as circles, slots as obrounds, pockets as their outline). side and top are the thickness rectangles with a hidden line per through feature — this is the case where a hand-drawing model reliably forgets the hidden lines.

Sections. mech_view_add(kind="section", plane=…, style="full"|"half"| "offset"|"revolved"). The engine decides per feature whether the plane cuts it; cut faces are closed boundaries handed to entity_create_hatch with the material's ISO 128-50 pattern; a threaded hole in section follows ISO 6410's section convention (hatch runs to the major diameter). Ribs and webs are not hatched when cut longitudinally (ISO 128-3) — declared by a no_section_hatch flag on the feature.

Details. kind="detail" copies a circular region at a larger scale, draws the ISO 128-34 detail circle and both labels (A on the parent, A (5:1) on the detail).

Projection layout. Views are placed in first-angle positions by default (the right view goes to the left of the front view); projection="third" flips them. The chosen convention is recorded in the title block symbol. Views stay aligned: a mech_view_add computes its origin from the parent view's bounding box plus a gap, so the alignment critique focus has something true to check.

The honesty rule. If a feature cannot be projected into the requested view, the view result lists it in omitted: [{feature, reason}] and the tool says so in its payload. Nothing is silently dropped, and nothing is approximated into a shape it is not.

7. Dimensioning

Each feature declares dimension intents — (kind, from, to, text override, placement preference). mech_dimension_part collects them, resolves ISO 286 fits through the existing engineering/fits.py, and lays them out: chain or baseline for the axial lengths, diameter dimensions stacked outside the silhouette by size, radius for fillets, a hole table when a prismatic part has more than hole_table_threshold (default 8) holes. It reuses the existing dimension_auto machinery and the ISO 129 tolerance path (tol_mode, fit=) rather than re-implementing either.

Overlap is not a matter of taste: the existing dim_overlap critique focus is the gate, and the layout algorithm iterates placement until that focus is silent or reports what it could not place.

8. Standard parts (SECTION 22)

std_part_list(query) searches the catalogue by designation, standard or size ("M12", "ISO 4014", "6205") and returns rows with the dimensions that matter for drawing. std_part_insert(designation, at, view, rotation, layer) builds the part once as a real block through block_define (typed primitives + ATTDEFs for designation/standard/size), inserts it, and writes ACADMCP_MECH XDATA so bom_extract finds it without a naming convention. Views: side (elevation) and top (plan) for fasteners; simplified and detailed (ISO 8826-1/-2) for bearings.

std_feature_draw(kind, …) draws a standard feature onto geometry that already exists — a thread on a drawn cylinder, a DIN 509 undercut at a drawn step, a retaining-ring groove, a centre hole, an O-ring groove — for the case where the part was not built from the model (a foreign drawing being finished).

9. Mechanical annotation (SECTION 23)

  • surface_texture(at, leader_to, ra|rz, process, lay, machining, all_around) — ISO 21920-1 symbol geometry derived from the current text height (60° V, proportions per the standard), with ISO 1302's older Ra form accepted as an alias for drawings that still use it.
  • weld_symbol(at, leader_to, kind, size, length, pitch, side, field_weld, all_around, process) — ISO 2553 reference line + dashed identification line, symbol above/below per side, staggered intermittent notation.
  • centre_marks(handles|centers, style="mark"|"lines", extension) — ISO 128-23 proportions on CENTER; the mech_missing_centreline focus is what makes it matter.
  • section_line(p1, p2, label, direction, style) — ISO 128-40 cutting-plane line with thick ends and arrows; returns the plane definition that mech_view_add(kind="section") consumes, so the label and the view cannot disagree.
  • hatch_material(boundary|handles, material, scale, angle) — the ISO 128-50 map (steel 45° ANSI31, cast iron ANSI32-style cross, aluminium, copper alloy, plastic/rubber, concrete, wood, insulation); an unknown material name is refused with the list, not silently hatched as steel.

10. Sheet and delivery (SECTION 24, track G)

  • sheet_frame(size, orientation, zones, marks) — ISO 5457 A4–A0: 20 mm filing margin, 10 mm elsewhere, zone grid (letters up the side, numbers along the top, 50 mm modules), centring marks, trimming marks. Geometry is pinned against the ISO 5457 table by test — the frame is the one thing every sheet has, so a wrong margin is visible on every drawing this server makes.
  • titleblock_apply(size, fields) — ISO 7200 data fields for every sheet size; the existing titleblock_apply_iso_a3 becomes a thin alias that calls it with size="A3" so no caller breaks. Carries the projection-angle symbol chosen by the part tools.
  • revision_add(rev, description, date, by, clouds, tags) — appends a row to the revision block (created on first use), optionally draws revision clouds over the given regions and triangular revision tags. Idempotent per revision letter.
  • bom_extract(scope, columns, group_by) — walks INSERTs' attributes and ACADMCP_MECH XDATA and returns parts-list rows (item, quantity, designation, standard, material, mass if known). Never modifies the drawing.
  • bom_table(rows|auto, at, standard="ISO 7573", direction) — draws the parts list as a real TABLE, growing upwards from the title block per ISO 7573.
  • balloon_add(item, at, leader_to, targets) — one ISO 6433 item reference per call (the caller loops over the rows bom_extract already numbered), linked to its targets by XDATA so a rerun renumbers in place instead of duplicating. The mech_bom_balloon_mismatch focus is what proves a balloon and its row agree.
  • data_extract(scope, format, path) — the same rows to CSV (always) or XLSX (refused with xlsx_write when openpyxl is absent, naming the extra).
  • xref_attach(path, at, scale, rotation, kind) — external reference as attachment or overlay; both engines (ezdxf add_xref_def + blockref).
  • xref_manage(name, action) — list / reload / bind / detach / path. list, detach and path work headlessly; reload and bind on a live seat or headlessly through ezdxf.xref where it is faithful — where it is not, the xref_live capability refuses honestly rather than pretending.
  • image_attach(path, at, scale, rotation) — raster underlay (add_image_def + add_image), both engines.
  • drawing_export_dwg(path, version) — COM SaveAs natively; headless through ezdxf.addons.odafc only when the ODA File Converter is installed (measured on this machine: it is not), otherwise the dwg_write capability refuses and names the install route. This was the user's explicit choice.

11. Tool surface, packs, profiles

Section Tools Pack
21 Mechanical parts mech_part_draw, mech_view_add, mech_dimension_part, mech_hole_pattern, mech_part_from_spec, mech_part_inspect mech
22 Standard parts std_part_list, std_part_insert, std_feature_draw mech
23 Mechanical annotation surface_texture, weld_symbol, centre_marks, section_line, hatch_material mech
24 Sheet and delivery sheet_frame, titleblock_apply, revision_add, bom_extract, bom_table, balloon_add, data_extract, xref_attach, xref_manage, image_attach, drawing_export_dwg core

TOOL_PACK_NAMES becomes ("core", "pid", "settings", "mech"). Lean gains five — mech_part_draw, mech_view_add, mech_dimension_part, sheet_frame, titleblock_apply — reaching 60; the catalogue, annotation and delivery tools stay full-profile, because a lean client draws a part and a frame and leaves the paperwork to a full one.

Discovery: every new tool gets an alias record with its AutoCAD command equivalents (SECTIONPLANE, XATTACH, IMAGEATTACH, TABLE, REVCLOUD, WBLOCK) and shop synonyms (mil/shaft, plaka/plate, flanş/flange, kama/keyway, pah/chamfer, kaynak/weld, yüzey pürüzlülüğü/surface finish, parça listesi/BOM); ten new golden queries join the tuning set.

12. Capabilities and refusals

Three keys, each a genuine engine boundary, declared in both maps (the tests/test_capability_contract.py gate):

Key ezdxf COM Reason
dwg_write ✗ (✓ if ODA installed) ✓ headless DWG needs the ODA File Converter, measured absent on this machine; the key's reason names the install route and system_capabilities re-evaluates it at connect time, the way viewport_render re-evaluates matplotlib
xref_live ✗ ✓ reload and bind need a live seat's xref manager; list, detach and path are headless and are not behind the key
xlsx_write ✓ only with openpyxl ✓ only with openpyxl an optional dependency, not an engine difference; same shape as the [pdf] extra, and CSV is always available so the refusal never blocks the deliverable

Refusals that are not capabilities — they are validation, and they fire before any write: an unknown material, a size outside a standard's table, a feature off the part, a non-finite number, an outline that self-intersects, a balloon without a parts-list row.

13. Critique focuses

Six new focuses in the closed enum, dispatched like the P&ID ones (they return nothing on a drawing with no mechanical content, so focus=None stays cheap):

  • mech_missing_centreline — a circle or a cylindrical silhouette with no centre mark or axis (ISO 128-23).
  • mech_unhatched_section — a view declared as a section whose cut faces carry no hatch.
  • mech_view_misaligned — two views of the same part off their projection axis by more than a tolerance, or placed in the wrong quadrant for the declared projection angle.
  • mech_duplicate_dimension — the same feature dimensioned twice (ISO 129-1: each dimension appears once).
  • mech_thread_unrepresented — a threaded feature drawn as a plain circle or cylinder with no ISO 6410 representation.
  • mech_bom_balloon_mismatch — a balloon with no parts-list row, a row with no balloon, or a quantity that disagrees with the INSERT count.

14. Evidence

  • Benchmark task mech_assembly (roadmap criterion 3): a flange-coupling sheet built from the tools — two parts, a section, a bolt circle with ISO 4014 bolts and ISO 4032 nuts, ISO 286 fits on the bearing seats, ISO 129 dimensions, ISO 5457 A3 frame, ISO 7200 title block, ISO 7573 parts list with balloons. Gate: drawing_critique(focus=None) returns 0 issues and drawing_finalize scores ≥ 90. Matrix v5 (benchmarks/tasks_v5.py, 18 tasks); the published competitor reports stay not run for it.
  • Correctness checks (suite grows 32 → 38): mech_part_roundtrip (draw → mech_part_inspect returns the same feature list), mech_section_hatch_area (the hatched area equals the computed cut area through analysis_measure_entity — a number that cannot be faked), mech_iso286_on_dimension (a ⌀40 H7 bore dimension carries +0.025/0), sheet_frame_iso5457_a3 (frame geometry against the standard's table), bom_balloon_link (every balloon resolves to its row after a renumber), std_part_iso4014_m12 (the inserted bolt's across-flats and head height match the table).
  • Live COM smoke scripts/smoke_mech_com.py on AutoCAD 2026: a shaft with a section, a standard bolt, a weld symbol, a frame, a parts list, an xref attach/detach and a real .dwg export — executed once, recorded in the CHANGELOG the way tracks A and E were.
  • A/B against v1.5.1 stays at zero regressions (release gate).

15. Testing

  • Pure modules (part, features, views, dimension, standards/*, sheet/frames, sheet/bom) are tested without a backend: given a model, assert the emitted primitive descriptions — count, layer, geometry to 1e-9. This is where the standards are pinned.
  • Provenance tests per table: named values from the standard, plus the coverage refusal at both ends of the range.
  • Backend integration per engine: ezdxf against a real document; COM against fakes that model the measured ActiveX member names (the Track E rule: a fake that invents a member enshrines a bug).
  • The critique focuses get a fixture drawing each, positive and negative.
  • Every task ends with the repository's gate set (ruff, inventory, registry, aliases, capability contract, release consistency).

16. Decisions recorded

  1. One part model, not per-part generators. The alternative (a shaft_draw/plate_draw/flange_draw zoo) duplicates the hidden-line, section and dimension logic three times and still cannot draw a flanged shaft. Named entry points remain for discoverability, but they build the same model.
  2. First angle by default, third angle available, the choice recorded in the title block symbol.
  3. Standard parts are blocks with attributes, exactly like the P&ID catalogue, so the parts list is a read of the drawing rather than a bookkeeping side-channel.
  4. The tables are transcribed, not derived. No interpolation; coverage is declared and refused at its edges.
  5. B and G ship together because criterion 3's sheet needs the frame, the title block, the parts list and the balloons to exist.