From 3248bfbfea50c5a9fe2c27db784fcc029b6796d6 Mon Sep 17 00:00:00 2001 From: Umut CAN Date: Wed, 23 Sep 2026 14:50:07 +0300 Subject: [PATCH] =?UTF-8?q?docs(plan):=20track=20F=20skeleton=20=E2=80=94?= =?UTF-8?q?=2010=20tasks=20in=20three=20groups,=20interfaces=20pinned?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Header, global constraints, file structure, the interface block every task builds against, the group/ownership table, the SECTION 25 banner protocol and the merge notes. Task sections are drafted next. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd --- .../2026-09-23-v1.6-arch-implementation.md | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-23-v1.6-arch-implementation.md diff --git a/docs/superpowers/plans/2026-09-23-v1.6-arch-implementation.md b/docs/superpowers/plans/2026-09-23-v1.6-arch-implementation.md new file mode 100644 index 0000000..53817d9 --- /dev/null +++ b/docs/superpowers/plans/2026-09-23-v1.6-arch-implementation.md @@ -0,0 +1,241 @@ +# 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. + +---