From 5ff66b641f6b588290d247cad8020306d00f80a1 Mon Sep 17 00:00:00 2001 From: Umut CAN Date: Tue, 22 Sep 2026 14:45:24 +0300 Subject: [PATCH] docs: tracks B+G (mechanical 2D and the sheet standard) design spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One part model — a revolved or prismatic profile plus typed features — and one view engine that projects it into front/side/section/detail views with hidden lines, cut faces and dimensions, instead of a generator per part shape. Around it: the standard-parts catalogue as code inserted as blocks with attributes, the ISO annotation symbols, and the sheet standard (ISO 5457 frames A4-A0, ISO 7200 title blocks, revisions, ISO 7573 parts list, balloons, xrefs, DWG). 25 tools in four sections, new pack `mech`, three capability keys, six mech_* critique focuses, benchmark task `mech_assembly` and six correctness checks. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd --- .../specs/2026-09-22-v1.6-mech-design.md | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-22-v1.6-mech-design.md diff --git a/docs/superpowers/specs/2026-09-22-v1.6-mech-design.md b/docs/superpowers/specs/2026-09-22-v1.6-mech-design.md new file mode 100644 index 0000000..55b0d07 --- /dev/null +++ b/docs/superpowers/specs/2026-09-22-v1.6-mech-design.md @@ -0,0 +1,461 @@ +# v1.6 Tracks B + G — the complete mechanical 2D part drawer and the sheet standard + +**Date:** 2026-09-22 · **Status:** design approved in conversation · +**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. + +## 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 + 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) + refs.py xref attach/list/reload/bind/detach, image attach + +backends/contracts/mech.py (new) part/view/dimension/standard-part/annotation members +backends/contracts/sheet.py (new) frame/titleblock/revision/bom/xref/image/dwg members +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 +``` + +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 + +```python +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 + origin: tuple[float, float] = (0.0, 0.0) # part-local; the tool places it in WCS + +@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" + origin: tuple[float, float] = (0.0, 0.0) +``` + +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 `Hole`s 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, position)` — 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(handles|rows, at, start, leader_to, arrangement)` — ISO 6433 item + references, auto-numbered to match the parts list, linked by XDATA so a rerun + renumbers instead of duplicating. +- `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, clip)` — 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.