Files
autocad-mcp-pro/docs/superpowers/plans/2026-09-22-v1.6-mech-implementation.md
T
Umut CANandClaude Opus 5 c97be8fc40 docs(plan): tracks B+G skeleton — 16 tasks in five groups, interfaces pinned
The header, global constraints, file structure, the interface block every task
builds against, the group/ownership table and the merge notes. Task sections
are drafted next.

Also corrects the spec's architecture section: the engineering tools compose
the generic contract members the way gear/keyway/titleblock already do, so only
four operations (xref_attach, xref_manage, image_attach, drawing_export_dwg)
need contract members and an implementation per engine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-22 14:56:37 +03:00

308 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) <noreply@anthropic.com>` 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 <changed files>` and
`uv run --frozen ruff format <changed files>`.
- **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": "<hex>", "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("<path>: <reason>")
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).
---