# Tracks B + G — mechanical 2D part drawer and sheet standard: implementation plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** One part model and one view engine that draw any revolved or prismatic machine part — with hidden lines, sections, standard features and ISO dimensions — plus the standard-parts catalogue, the ISO annotation symbols and the sheet standard (frames, title blocks, revisions, parts list, balloons, xrefs, DWG), as 25 tools on both engines. **Architecture:** Pure geometry modules (`engineering/mech/*`, `engineering/sheet/*`) emit typed primitive *descriptions*; one async layer (`draw.py` and the server tools) turns them into backend calls, exactly the way `gear_draw_*` and `titleblock_apply_iso_a3` already work. Only four operations differ per engine and become contract members (`xref_attach`, `xref_manage`, `image_attach`, `drawing_export_dwg`). **Tech Stack:** Python 3.11, FastMCP 3, ezdxf 1.4.x, pywin32 (COM), pytest + pytest-asyncio, ruff. Everything runs through `uv run --frozen`. **Spec:** `docs/superpowers/specs/2026-09-22-v1.6-mech-design.md` — read it before your task; this plan argues from it and does not repeat its reasoning. ## Global Constraints - **Run everything through `uv run --frozen …`.** Never `uv sync` (the shell's bare `python` is 3.14 without dependencies). If `uv run --frozen` fails with `os error 32` on `.venv/Scripts/autocad-mcp.exe`, a running MCP server holds the launcher: use `uv run --frozen --no-sync …` for the rest of the task and say so in `deviations`. - **Commit** with `git -c user.name="Umut CAN" -c user.email="umutcan.edizaslan@gmail.com" commit` and end every message with these two lines verbatim: `Co-Authored-By: Claude Opus 5 (1M context) ` and `Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd`. - **Known red that is not yours:** `tests/test_release_consistency.py::test_document_numbers_match_the_tree` drifts on the README "collected tests" figure whenever tests are added. Deselect exactly that test in a worktree and say so; every other test in that file must pass. Task 15 retakes the figure on main. - **The table rule (spec §5).** Every row of every table under `engineering/mech/standards/` is transcribed from the named standard's own table, carries a `SOURCE` docstring naming standard + edition + table number, and is pinned by a provenance test asserting named values. A size outside the transcribed range is refused by name — never interpolated. **If you cannot verify a row, do not ship the row**: narrow the coverage, say so in the docstring and in `deviations`. - **Validate before writing** (CLAUDE.md): every refusal fires before the first entity reaches the drawing, names the offending path (`segments[2].d_outer`), and leaves the drawing untouched. - **WCS only.** Every coordinate in or out of a tool is WCS. Part-local coordinates exist inside the pure modules; `draw.py` is the only place that maps them, through the part's placement (`at`, `rotation`). - **No silent wrong number.** A feature that cannot be projected into a view is reported in `omitted: [{"feature": …, "reason": …}]`, never dropped. - **Layers** come from the role map in `engineering/mech/primitives.py` (`visible→GEOMETRY`, `hidden→HIDDEN`, `center→CENTER`, `phantom→PHANTOM`, `hatch→HATCH`, `dim→DIM`, `text→TEXT`); no tool hardcodes a layer name. - **Before committing:** `uv run --frozen ruff check ` and `uv run --frozen ruff format `. - **Registration gates** (only when you register a tool): regenerate `docs/tool-inventory.json` with `uv run --frozen python scripts/generate_tool_inventory.py`, bump the README release-snapshot tool count, bump the frozen group-size snapshot in `tests/test_tool_registry.py`, add an alias record per tool in `discovery/aliases.py` (`tests/test_discovery_aliases.py` fails otherwise). - **Deviations are data.** If a plan test asserts something the real API cannot satisfy, verify the truth by running code, fix the *test* to that truth while keeping the behaviour the spec demands, and record it with the evidence. Never delete, skip or weaken a test to get green. --- ## File Structure | File | Responsibility | Task | Group | |---|---|---|---| | `engineering/mech/primitives.py` | typed primitive descriptions + role→layer map | 1 | shared | | `engineering/mech/xdata.py` | chunked JSON codec for the `ACADMCP_MECH` payload | 1 | shared | | `engineering/mech/standards/__init__.py` | table registry + `lookup()` + coverage refusal | 1 | shared | | `engineering/mech/standards/materials.py` | ISO 128-50 material → hatch pattern/angle/scale | 1 | shared | | `engineering/mech/part.py` | `RevolvedPart`, `PrismaticPart`, `build_part`, validation | 2 | M | | `engineering/mech/features.py` | every feature dataclass, its validation and geometry | 3 | M | | `engineering/mech/standards/threads.py` | ISO 261/965 pitch + ISO 6410 representation rules | 3 | M | | `engineering/mech/standards/undercuts.py` | DIN 509 forms E/F | 3 | M | | `engineering/mech/standards/rings.py` | DIN 471 / DIN 472 groove dimensions | 3 | M | | `engineering/mech/standards/centres.py` | DIN 332 forms A/B/R | 3 | M | | `engineering/mech/standards/orings.py` | ISO 3601-2 groove dimensions | 3 | M | | `engineering/mech/views.py` | the view engine: orthographic (4), sections + details (5) | 4, 5 | M | | `engineering/mech/dimension.py` | dimension intents + overlap-free layout | 6 | M | | `engineering/mech/draw.py` | the async layer: primitives → backend calls | 7 | M | | `engineering/mech/standards/fasteners.py` | ISO 4014/4017/4032/7089/4762 tables | 8 | S | | `engineering/mech/standards/bearings.py` | ISO 15 series + ISO 8826-1 representation | 8 | S | | `engineering/mech/stdparts.py` | catalogue → block specs → insert + XDATA | 9 | S | | `engineering/mech/annotate.py` | surface texture (ISO 21920-1), weld (ISO 2553) | 10 | A | | `engineering/mech/marks.py` | centre marks, section line, material hatch | 11 | A | | `engineering/sheet/frames.py` | ISO 5457 sizes, margins, zones, centring/trim marks | 12 | G | | `engineering/sheet/titleblock.py` | ISO 7200 for A4–A0 (today's A3 module folds in) | 12 | G | | `engineering/sheet/revision.py` | revision block rows, clouds, triangle tags | 13 | G | | `engineering/sheet/bom.py` | extract → rows → ISO 7573 table; ISO 6433 balloons | 13 | G | | `engineering/sheet/extract.py` | rows → CSV (always) / XLSX (optional dependency) | 14 | G | | `backends/contracts/refs.py` | the four engine-specific members | 14 | G | | `backends/ezdxf_backend.py` | their ezdxf implementations | 14 | G | | `backends/com_backend.py` | their COM implementations | 14 | G | | `engineering/mech/critique.py` | the six `mech_*` critique focuses | 15 | F | | `server.py` | SECTION 21 (7) · 22 (9) · 23 (10, 11) · 24 (12, 13, 14) | per task | per group | | `benchmarks/tasks_v5.py`, `benchmarks/correctness_suite.py` | evidence | 16 | F | | `scripts/smoke_mech_com.py` | live COM execution, once | 16 | F | ## Interfaces (the contract between tasks) Task 1 lands on `main` before the groups branch; every other task consumes it. ```python # engineering/mech/primitives.py Role = Literal["visible", "hidden", "center", "phantom", "hatch", "dim", "text"] ROLE_LAYER: dict[Role, str] # visible→GEOMETRY, hidden→HIDDEN, center→CENTER, # phantom→PHANTOM, hatch→HATCH, dim→DIM, text→TEXT Pt = tuple[float, float] class Line(NamedTuple): p1: Pt; p2: Pt; role: Role = "visible" class Arc(NamedTuple): center: Pt; radius: float; start_deg: float; end_deg: float; role: Role = "visible" class Circle(NamedTuple): center: Pt; radius: float; role: Role = "visible" class Poly(NamedTuple): points: tuple[Pt, ...]; closed: bool = False; role: Role = "visible" class HatchArea(NamedTuple): loops: tuple[tuple[Pt, ...], ...] # [0] outer, [1:] islands material: str = "steel" class Text(NamedTuple): at: Pt; text: str; height: float = 3.5; rotation: float = 0.0; role: Role = "text" class DimIntent(NamedTuple): kind: Literal["linear", "aligned", "diameter", "radius", "angular"] p1: Pt; p2: Pt text_at: Pt | None = None text_override: str | None = None fit: str | None = None # e.g. "H7" → resolved by engineering/fits.py tol: dict | None = None # {"mode": …, "upper": …, "lower": …} → engineering/tolerances.py feature: str = "" # the feature id this dimension belongs to Prim = Line | Arc | Circle | Poly | HatchArea | Text def bbox(prims: Iterable[Prim]) -> tuple[float, float, float, float] # (minx, miny, maxx, maxy) def translate(prims: Iterable[Prim], dx: float, dy: float) -> tuple[Prim, ...] def rotate(prims: Iterable[Prim], deg: float, about: Pt = (0.0, 0.0)) -> tuple[Prim, ...] def scale(prims: Iterable[Prim], factor: float, about: Pt = (0.0, 0.0)) -> tuple[Prim, ...] # engineering/mech/xdata.py APP_ID = "ACADMCP_MECH" PAYLOAD_VERSION = 1 def encode(payload: dict) -> list[tuple[int, str]] # 1001 app id + 1000 chunks ≤ 255 chars def decode(rows: Sequence[tuple[int, str]]) -> dict # raises ValueError on a foreign/corrupt payload # Payload shapes (pinned; both the writer and every reader test them literally): # {"v": 1, "kind": "part", "id": "", "view": "front", "part": {...part_to_dict...}} # {"v": 1, "kind": "std_part", "designation": "ISO 4014 - M12x60", "standard": "ISO 4014", # "size": "M12x60", "qty": 1, "material": "8.8"} # {"v": 1, "kind": "balloon", "item": 3, "targets": ["1A2B", …]} # engineering/mech/standards/__init__.py class Coverage(NamedTuple): low: float; high: float; unit: str = "mm" def register(standard: str, rows: dict, coverage: Coverage, source: str) -> None def lookup(standard: str, size, **kw) -> dict # ValueError naming standard + coverage when outside def standards() -> tuple[str, ...] # engineering/mech/standards/materials.py def hatch_for(material: str) -> dict # {"pattern": "ANSI31", "angle": 45.0, "scale": 1.0} MATERIALS: tuple[str, ...] # ValueError lists these when the name is unknown ``` ```python # engineering/mech/part.py (Task 2) class Segment(NamedTuple): length: float; d_outer: float; d_inner: float = 0.0; taper_to: float | None = None @dataclass(frozen=True) class RevolvedPart: name: str; segments: tuple[Segment, ...]; features: tuple[Feature, ...] = () material: str = "steel" @dataclass(frozen=True) class PrismaticPart: name: str; outline: tuple[Pt, ...]; thickness: float; features: tuple[Feature, ...] = () material: str = "steel" Part = RevolvedPart | PrismaticPart def build_part(spec: dict) -> Part # validates; ValueError(": ") def part_to_dict(part: Part) -> dict # round-trips through build_part def part_length(part: Part) -> float def part_max_diameter(part: RevolvedPart) -> float # engineering/mech/features.py (Task 3) @runtime_checkable class Feature(Protocol): id: str def validate(self, part) -> None # ValueError naming the path def prims(self, part, view: str, ctx: dict) -> tuple[Prim, ...] def cut_prims(self, part, plane, ctx: dict) -> tuple[Prim, ...] def dims(self, part, view: str, ctx: dict) -> tuple[DimIntent, ...] def feature_from_dict(d: dict) -> Feature # the "kind" key selects the class FEATURE_KINDS: tuple[str, ...] # engineering/mech/views.py (Tasks 4, 5) @dataclass(frozen=True) class View: kind: Literal["front", "side", "top", "section", "detail"] prims: tuple[Prim, ...] dims: tuple[DimIntent, ...] omitted: tuple[dict, ...] bbox: tuple[float, float, float, float] label: str | None = None scale: float = 1.0 def build_view(part, kind, *, side="right", plane=None, style="full", scale=1.0, projection="first", detail=None) -> View def layout_origin(parent: View, kind, *, side="right", projection="first", gap=20.0) -> Pt SECTION_PLANE = {"p1": (0.0, 0.0), "p2": (0.0, 0.0), "label": "A", "direction": (0.0, -1.0), "style": "full"} # the shape Task 11 produces # engineering/mech/dimension.py (Task 6) def dimension_intents(part, view: View, *, style="chain", hole_table_threshold=8) -> tuple[DimIntent, ...] def layout_dimensions(view: View, intents, *, offset=10.0, step=8.0) -> tuple[DimIntent, ...] def hole_table_rows(part, view: View) -> tuple[dict, ...] # {"tag","x","y","d","qty","note"} # engineering/mech/draw.py (Task 7) async def draw_prims(backend, prims, *, at=(0.0, 0.0), rotation=0.0, layer_prefix="") -> dict async def draw_part(backend, part, *, at=(0.0, 0.0), rotation=0.0, views=("front",), projection="first", dimension=True) -> dict # {"part_id","views":[…],"handles":[…]} async def add_view(backend, part_id, kind, **kw) -> dict async def dimension_part(backend, part_id, *, style="chain") -> dict async def read_part(backend, part_id=None) -> dict # {"part_id","part",…} from XDATA ``` ```python # engineering/mech/stdparts.py (Task 9) def catalogue(query: str | None = None, standard: str | None = None) -> tuple[dict, ...] def block_spec(designation: str, view: str) -> dict # for block_define async def insert_std_part(backend, designation, *, at, view="side", rotation=0.0, layer="GEOMETRY", attributes=None) -> dict # engineering/mech/annotate.py (Task 10) def surface_texture_prims(*, ra=None, rz=None, process=None, lay=None, machining="any", all_around=False, height=3.5) -> tuple[Prim, ...] def weld_symbol_prims(*, kind, size=None, length=None, pitch=None, side="arrow", field_weld=False, all_around=False, process=None, height=3.5) -> tuple[Prim, ...] # engineering/mech/marks.py (Task 11) def centre_mark_prims(center: Pt, radius: float, *, style="mark", extension=3.0) -> tuple[Prim, ...] def section_line_prims(p1: Pt, p2: Pt, *, label="A", direction=(0.0, -1.0), height=5.0) -> tuple[Prim, ...] # engineering/sheet/frames.py (Task 12) SHEETS: dict[str, tuple[float, float]] # "A4"…"A0" → (w, h) portrait, ISO 216 def frame_prims(size: str, *, orientation="landscape", zones=True, marks=True) -> tuple[Prim, ...] def frame_metrics(size: str, *, orientation="landscape") -> dict # margins, drawing area, zone grid # engineering/sheet/titleblock.py (Task 12) def titleblock_prims(size: str, fields: dict, *, orientation="landscape", projection="first") -> tuple[Prim, ...] TITLEBLOCK_FIELDS: tuple[str, ...] # ISO 7200 data fields # engineering/sheet/bom.py (Task 13) def rows_from_records(records: Sequence[dict], *, columns=None, group_by="designation") -> tuple[dict, ...] def table_prims(rows, *, at, direction="up", standard="ISO 7573", height=7.0) -> tuple[Prim, ...] def balloon_prims(item: int, at: Pt, leader_to: Pt, *, radius=4.0) -> tuple[Prim, ...] # backends/contracts/refs.py (Task 14) async def xref_attach(self, path: str, at, scale: float, rotation: float, kind: str) -> dict async def xref_manage(self, name: str, action: str, new_path: str | None) -> dict async def image_attach(self, path: str, at, scale: float, rotation: float) -> dict async def drawing_export_dwg(self, path: str, version: str) -> dict ``` ## Execution | Group | Branch / worktree | Tasks | Owns | |---|---|---|---| | **(main)** | `main` | 1 | `engineering/mech/{primitives,xdata}.py`, `standards/{__init__,materials}.py` | | **M** | `track-bg/mech` | 2, 3, 4, 5, 6, 7 | `engineering/mech/{part,features,views,dimension,draw}.py`, `standards/{threads,undercuts,rings,centres,orings}.py`, server SECTION 21 | | **S** | `track-bg/std` | 8, 9 | `engineering/mech/stdparts.py`, `standards/{fasteners,bearings}.py`, server SECTION 22 | | **A** | `track-bg/annot` | 10, 11 | `engineering/mech/{annotate,marks}.py`, server SECTION 23 | | **G** | `track-bg/sheet` | 12, 13, 14 | `engineering/sheet/*`, `backends/contracts/refs.py`, both backends, server SECTION 24 | | **F** | `main` (after merge) | 15, 16 | critique focuses, packs/lean/aliases/golden, benchmarks, smoke, docs | Task 1 lands on `main` first; the four group worktrees branch from that commit and run in parallel. Tasks inside a group are sequential. ### Merge notes (for the agent that merges the four branches) Merge order **M → S → A → G**. Expected conflicts, all anticipated: - `server.py`: each group appends its section before the `# ---` RESOURCES banner. Keep all four, in order 21 → 22 → 23 → 24, and restore the banner at the seam. - `engineering/mech/__init__.py`: additive re-exports — keep every side's names, alphabetical. - `discovery/aliases.py`: additive — keep every group's alias records and merge the `SHARED_ACAD_COMMANDS` additions alphabetically. - `docs/tool-inventory.json`: regenerate after resolving the code (`uv run --frozen python scripts/generate_tool_inventory.py`), never hand-merge. - `README.md`: the release-snapshot line — recompute tools/resources from the regenerated inventory; leave the collected-tests figure to Task 15. Keep every group's "What you get" row. - `tests/test_tool_registry.py`: recompute the frozen group-size snapshot from the live registry; concatenate the docstring paragraphs. - `backends/base.py` and the two capability maps: only group G touches them (`dwg_write`, `xref_live`, `xlsx_write`) — take G's side whole. - Any conflict in a file **not** listed: `git merge --abort`, record the branch as not merged, continue with the next branch. After the merges: `uv run --frozen ruff check .`, the full suite, and the gate set (inventory `--check`, `tests/test_tool_registry.py`, `tests/test_capability_contract.py`, `tests/test_discovery_aliases.py`, `tests/test_release_consistency.py` — its collected-tests drift is Task 15's). ---