# Track F — architectural 2D (the plan model): 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:** A plan model (a wall network with hosted doors and windows) and one engine that draws it with clean junctions, cut openings and material poché, computes rooms as the faces the walls enclose, reads rooms from foreign plans, and delivers schedules, a furniture/sanitary catalogue, a grid, symbols and dimension chains — 12 tools on both engines. **Architecture:** Pure modules under `engineering/arch/` emit the typed primitives of `engineering/mech/primitives.py` with architectural roles; the async layer draws them through `engineering.mech.draw.draw_prims` (generalised in Task 1 to take a role → layer map), composing only generic contract members — no new contract member, so both engines work by construction. **Tech Stack:** Python 3.11, FastMCP 3, ezdxf 1.4.x, pywin32 (COM), pytest + pytest-asyncio, ruff — everything through `uv run --frozen`. **Spec:** `docs/superpowers/specs/2026-09-23-v1.6-arch-design.md` — read it before your task. ## Global Constraints - **Run everything through `uv run --frozen …`**, never `uv sync`. If it fails with `os error 32` on `.venv/Scripts/autocad-mcp.exe`, use `uv run --frozen --no-sync …` for the rest of the task and say so. - **Commit** with `git -c user.name="Umut CAN" -c user.email="umutcan.edizaslan@gmail.com" commit`, message ending with these two lines verbatim: `Co-Authored-By: Claude Opus 5.5 (1M context) ` and `Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd`. - **Known red:** `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. Task 9 retakes the figure on main. - **Units are millimetres, coordinates are WCS**, angles in degrees CCW from +X. A wall's `justification` is the side of its axis the wall lies on, *looking along the axis from its first point*: `left`, `right` or `center`. - **Validate before writing**: every refusal fires before the first entity, names its path (`walls[2].thickness`), and leaves the drawing untouched. - **Areas are measured, never typed.** A room's area comes from the planar face its label point lies in; no tool accepts an area from its caller. - **Language**: `lang` is `"en"` (default) or `"tr"`; `"tr"` also switches the decimal separator to a comma. Unknown values are refused with the list. - **Text heights follow the plot scale**: `scale` is a denominator (50 → 1:50); a paper height `h` mm is drawn at `h * scale` drawing units. No annotative objects (recorded out of 1.6 in track E). - **Layers** come from `engineering/arch/layers.py::ARCH_ROLE_LAYER`; no tool hardcodes a layer name. - **No standards value is invented.** Catalogue sizes are *nominal catalogue dimensions*, stated as such in the data and the tool docstring. The Blondel range 600–650 mm is a design rule the stair tool reports, not a code. - **Before committing:** `uv run --frozen ruff check ` and `uv run --frozen ruff format `. - **Registration gates** (when you register a tool): regenerate `docs/tool-inventory.json` (`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`. - **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 spec's behaviour, and record it. Never weaken a test. --- ## File Structure | File | Responsibility | Task | Group | |---|---|---|---| | `engineering/mech/draw.py` | `draw_prims` gains `role_layer=` | 1 | main | | `engineering/mech/xdata.py` | codec gains `app_id=` / `kinds=` (mech defaults unchanged) | 1 | main | | `engineering/layers.py` | `LAYER_SET_REGISTRY["arch"]`, `LAYER_ROLES["arch"]` | 1 | main | | `engineering/arch/__init__.py` | public re-exports | 1 (+ each task appends) | all | | `engineering/arch/layers.py` | `ARCH_LAYERS`, `ARCH_ROLE_LAYER` | 1 | main | | `engineering/arch/materials.py` | wall material → ISO 128-50 hatch | 1 | main | | `engineering/arch/lang.py` | EN/TR vocabulary, tag prefixes, number formatting | 1 | main | | `engineering/arch/model.py` | Wall/Opening/Stair/Room, validation, `ACADMCP_ARCH` payloads | 1 | main | | `engineering/arch/walls.py` | outlines, L/T/X junctions, opening gaps, poché | 2 | W | | `engineering/arch/openings.py` | door + window geometry | 3 | W | | `engineering/arch/stairs.py` | straight/L/U stairs, Blondel | 3 | W | | `engineering/arch/dimension.py` | exterior dimension chains | 4 | W | | `engineering/arch/draw.py` | async: walls, openings, stairs, chains | 4 | W | | `engineering/arch/faces.py` | planar-face finder (DCEL) | 5 | R | | `engineering/arch/rooms.py` | room labels (measured), foreign-plan reader | 5 | R | | `engineering/arch/schedule.py` | door/window/room schedules | 6 | R | | `engineering/arch/catalogue.py` | furniture + sanitary blocks | 7 | C | | `engineering/arch/symbols.py` | grid, north arrow, section/level/elevation marks | 8 | C | | `engineering/arch/critique.py` | three `arch_*` focuses | 9 | final | | `engineering/arch/spec.py` | `arch_plan_from_spec` | 9 | final | | `server.py` SECTION 25 | W opens the banner (Task 4); R and C append blocks | 4–8 | W/R/C | ## Interfaces (the contract between tasks) ```python # engineering/mech/draw.py (Task 1 — generalised, mechanical callers unchanged) async def draw_prims(backend, prims, *, at=(0.0, 0.0), rotation=0.0, layer_prefix="", role_layer: Mapping[str, str] | None = None) -> dict # role_layer maps Prim.role -> layer; None keeps the mechanical ROLE_LAYER. # Every target layer is ensured before the first entity (live seats refuse # entity.Layer on a missing layer). A layer named in ARCH_LAYERS is created # with that row's colour, linetype and lineweight. # engineering/mech/xdata.py (Task 1) def encode(payload, *, app_id=APP_ID, kinds=KINDS) -> list[tuple[int, str]] def to_values(payload, *, app_id=APP_ID, kinds=KINDS) -> list[str] def decode(rows, *, app_id=APP_ID, kinds=KINDS) -> dict # engineering/arch/layers.py (Task 1) ARCH_LAYERS: list[tuple[str, int, str, float, str]] # (name, aci, linetype, lineweight_mm, description) ARCH_ROLE_LAYER: dict[str, str] = { "wall": "A-WALL-E-N", "wall_hatch": "A-WALL-H-N", "door": "A-DOOR-E-N", "window": "A-WIND-E-N", "stair": "A-STAIR-E-N", "furniture": "A-FURN-E-N", "sanitary": "A-SANR-E-N", "grid": "A-GRID-E-N", "room": "A-ROOM-T-N", "dim": "A-DIMEN-T-N", "symbol": "A-SYMB-T-N", "overhead": "A-OVHD-E-N", } # engineering/layers.py: LAYER_SET_REGISTRY["arch"] = ARCH_LAYERS; # LAYER_ROLES["arch"] = {"dim": "A-DIMEN-T-N", "construction": "A-CONST-E-N"} # engineering/arch/materials.py (Task 1) WALL_MATERIALS = ("brick", "aac", "concrete", "reinforced_concrete", "gypsum_board", "stone", "timber", "generic") def wall_hatch(material: str) -> dict # {"pattern": str, "angle": float, "scale": float}; ValueError lists WALL_MATERIALS # engineering/arch/lang.py (Task 1) LANGS = ("en", "tr") def vocab(lang: str) -> dict[str, str] # keys: door_schedule, window_schedule, room_schedule, tag, # width, height, sill, swing, hand, wall, number, name, area, # total, up, down, north, level, in, out, left, right def tag_prefix(kind: str, lang: str) -> str # door → "D" | "K", window → "W" | "P" def fmt_number(value: float, lang: str, decimals: int = 2) -> str # "24.50" | "24,50" def fmt_area_m2(area_mm2: float, lang: str) -> str # "24.50 m²" | "24,50 m²" # engineering/arch/model.py (Task 1) APP_ID = "ACADMCP_ARCH" KINDS = ("wall", "opening", "stair", "room") @dataclass(frozen=True) class Wall: id: str; axis: tuple[Pt, ...]; thickness: float; justification: str = "center"; material: str = "brick"; closed: bool = False @dataclass(frozen=True) class Opening: id: str; wall: str; kind: str; offset: float; width: float; swing: str = "in"; hand: str = "left"; sill: float | None = None; height: float | None = None; tag: str | None = None @dataclass(frozen=True) class Stair: id: str; start: Pt; direction_deg: float; width: float; risers: int; riser_height: float; going: float; kind: str = "straight"; turn: str = "left" @dataclass(frozen=True) class Room: id: str; name: str; number: str | None; at: Pt; area: float = 0.0 def wall_from_dict(d: dict, path: str = "wall") -> Wall # ValueError(".: ") def opening_from_dict(d: dict, path: str = "opening") -> Opening def stair_from_dict(d: dict, path: str = "stair") -> Stair def room_from_dict(d: dict, path: str = "room") -> Room def validate_openings(walls: Sequence[Wall], openings: Sequence[Opening]) -> None # an opening on an unknown wall, past its segment, straddling an axis vertex, # or overlapping another opening on the same wall -> ValueError naming both ids def segment_of(wall: Wall, offset: float) -> tuple[int, float] # (segment index, distance along it) def to_payload(obj) -> dict # {"v": 1, "kind": "wall"|"opening"|"stair"|"room", **fields} def from_payload(payload: dict) # -> Wall | Opening | Stair | Room def encode(payload) / to_values(payload) / decode(rows) # the mech codec with APP_ID/KINDS above # engineering/arch/walls.py (Task 2) @dataclass(frozen=True) class WallGeometry: faces: tuple[Line, ...] # role "wall": every face segment actually drawn jambs: tuple[Line, ...] # role "wall": the short closing lines at opening edges and free ends regions: tuple[tuple[Pt, ...], ...] # closed cut-face polygons (CCW), one per solid wall piece hatches: tuple[HatchArea, ...] # one per region, material from wall_hatch (empty when poche=False) omitted: tuple[dict, ...] # [{"element": id, "reason": str}] def wall_geometry(walls, openings=(), *, poche=True, join_tol=1.0) -> WallGeometry def room_segments(geometry: WallGeometry) -> tuple[tuple[Pt, Pt], ...] # faces + jambs, for faces.py # engineering/arch/openings.py (Task 3) def opening_prims(wall: Wall, opening: Opening) -> tuple[Prim, ...] # door: leaf + quarter swing arc (role "door"); window: frame + two glazing lines (role "window") # engineering/arch/stairs.py (Task 3) def blondel(riser_height: float, going: float) -> dict # {"value": 2R+G, "ok": 600<=v<=650, "range": [600.0, 650.0]} def stair_prims(stair: Stair, *, lang="en", scale=50) -> tuple[Prim, ...] # treads, outline, walking line + arrow, "UP"/"ÇIKIŞ" label (role "stair") # engineering/arch/dimension.py (Task 4) def exterior_chains(walls, openings, *, sides=("bottom", "left"), first_offset=None, step=None, scale=50) -> tuple[DimIntent, ...] # engineering/arch/draw.py (Task 4) async def draw_walls(backend, walls, openings=(), *, poche=True) -> dict # {"walls": [...], "openings": [...], "handles": [...], "omitted": [...]} async def draw_stair(backend, stair, *, lang="en", scale=50) -> dict async def draw_chains(backend, walls, openings, *, sides=("bottom","left"), scale=50) -> dict async def read_plan(backend) -> dict # {"walls": [Wall], "openings": [Opening], "stairs": [...], "rooms": [...]} from ACADMCP_ARCH # engineering/arch/faces.py (Task 5) @dataclass(frozen=True) class Face: loop: tuple[Pt, ...]; area: float; centroid: Pt def planar_faces(segments, *, tol=1.0) -> tuple[Face, ...] # bounded faces only, CCW loops, area in mm² def face_containing(faces, point) -> Face | None # the smallest face containing the point # engineering/arch/rooms.py (Task 5) def room_label_prims(room: Room, *, lang="en", scale=50) -> tuple[Prim, ...] # name, number, measured area (role "room") def detect_rooms(segments, *, min_area=1.0e6, tol=1.0) -> tuple[dict, ...] # {"loop", "area", "centroid", "confidence"} async def label_room(backend, room_dict, *, lang="en", scale=50) -> dict # reads wall faces from the drawing, measures, draws, writes the room record async def rooms_detect(backend, *, layers=None, min_area=1.0e6, tol=1.0) -> dict # never modifies the drawing # engineering/arch/schedule.py (Task 6) def schedule_rows(kind: str, plan: dict, *, lang="en") -> tuple[dict, ...] async def draw_schedule(backend, kind, *, at, lang="en", scale=50) -> dict # engineering/arch/catalogue.py (Task 7) def catalogue(query: str | None = None, family: str | None = None, *, lang="en") -> tuple[dict, ...] def block_spec(name: str) -> dict # {"name", "entities", "attdefs"} for block_define async def insert_item(backend, name, *, at, rotation=0.0) -> dict # engineering/arch/symbols.py (Task 8) def grid_prims(x_axes, y_axes, *, labels=None, extension=1500.0, scale=50) -> tuple[Prim, ...] def symbol_prims(kind: str, at: Pt, *, lang="en", scale=50, **params) -> tuple[Prim, ...] # kind: "north_arrow" (rotation), "section_mark" (p1, p2, label), "level" (value), "elevation_mark" (label, directions) ``` ## Execution | Group | Branch / worktree | Tasks | Owns | |---|---|---|---| | **(main)** | `main` | 1 | `engineering/arch/{__init__,layers,materials,lang,model}.py`, the two mech generalisations, `engineering/layers.py` | | **W** | `track-f/walls` | 2, 3, 4 | `walls.py`, `openings.py`, `stairs.py`, `dimension.py`, `draw.py`; server SECTION 25 banner + wall block | | **R** | `track-f/rooms` | 5, 6 | `faces.py`, `rooms.py`, `schedule.py`; server SECTION 25 rooms block | | **C** | `track-f/catalogue` | 7, 8 | `catalogue.py`, `symbols.py`; server SECTION 25 catalogue block | | **final** | `main` (after merge) | 9, 10 | `spec.py`, `critique.py`, pack/lean/aliases/golden, benchmark, smoke, docs | Task 1 lands on `main` first; the three worktrees branch from that commit. Tasks inside a group are sequential. **SECTION 25 in `server.py`.** Group W opens it in Task 4 with the banner `# SECTION 25: Architecture (4 tools)` followed by its four tools, then a line `# ── arch: rooms (group R) ──` and a line `# ── arch: catalogue (group C) ──` with nothing under them. Group R adds its three tools under the rooms marker and group C its four under the catalogue marker — **on their own branches, where the markers do not exist yet**, so each of R and C also opens the section: R writes the banner `# SECTION 25: Architecture (3 tools)` + its marker + its tools; C the banner `(4 tools)` + its marker + its tools. The merge keeps one banner (count 11 after the merge; Task 9 adds the twelfth) and the three blocks in the order walls → rooms → catalogue. ### Merge notes Order **W → R → C**. Expected conflicts, all anticipated: `server.py` SECTION 25 (one banner, three blocks, as above; the RESOURCES banner restored at the seam), `engineering/arch/__init__.py` (additive re-exports), `discovery/aliases.py` (additive), `docs/tool-inventory.json` (regenerate, never hand-merge), README release-snapshot tool count (recompute from the inventory; the collected-tests figure is Task 9's), `tests/test_tool_registry.py` group sizes (recompute from the live registry). Any other conflict: `git merge --abort`, record the branch, continue. ---