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
This commit is contained in:
co-authored by
Claude Opus 5
parent
5ff66b641f
commit
c97be8fc40
@@ -0,0 +1,307 @@
|
||||
# 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).
|
||||
|
||||
---
|
||||
@@ -78,6 +78,8 @@ engineering/mech/
|
||||
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/
|
||||
@@ -99,12 +101,22 @@ engineering/sheet/
|
||||
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
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user