← Skills

SIP Decomposition

Native Read-only

Agent naming conventions, dependency ordering, coordinate system, and decomposition rules for SIP building manifests. Injected at manifest_generation and design_spec_generate.

/skills/sip_decomposition.md

Estimated tokens
6006
Characters
24024
Source
Native

Markdown

# SIP Building Decomposition Skill

This skill is used during **manifest generation and design spec creation**. It covers how to decompose a SIP building brief into a set of component agents, naming conventions, ordering rules, and coordinate system. It does not contain FreeCAD construction geometry — that is in the domain-specific skills (sip_walls, sip_roofs, etc.).

---

## When to Apply

Apply this skill when the project involves SIP construction: garden rooms, sheds, studios, cabins, or any non-habitable structure where walls and roof are built from structural insulated panels. If the brief mentions "SIP", "structural insulated panel", "wall panel", or "roof panel", this skill applies.

---

## Complete SIP Building — Required Agents

A complete SIP building manifest MUST include all of the following agent types. A manifest missing any of these is incomplete.

| Agent type | Required count | Purpose |
|---|---|---|
| `foundation` | exactly 1 | Concrete slab or strip foundation |
| `*_wall` | 4 (all perimeter walls) | South, north, east, west walls |
| `*_roof` | 1+ (depends on roof type) | Roof assembly |
| `ridge_beam` | 1 for duo-pitch roofs | Structural beam at the apex (see Ridge beam component) |
| ~~`*_fasteners` / `*_anchor_bolts` / `*_ties`~~ | **0 — do not emit** | Temporarily excluded; to be placed deterministically. See Fastener agents below |

**All four perimeter walls are mandatory.** A brief that mentions only one wall means the other three must still be included. A manifest with fewer than four wall agents is wrong.

---

## ⚠ NON-NEGOTIABLE: Agent Naming

These naming rules are enforced by validators. Violating them produces validation failures.

### Foundation agent

The foundation agent MUST be named exactly:

```
"agent_id": "foundation"
```

The following names are **FORBIDDEN** — they will fail validation:

- ❌ `concrete_slab`
- ❌ `slab`
- ❌ `strip_foundation`
- ❌ `foundation_slab`
- ❌ `base_plate` or `base_plate_*`
- ❌ `foundation_front`, `foundation_south`, `foundation_north`, etc.

**There is exactly one foundation agent and its id is `foundation`.** No exceptions.

**Wrong:**
```json
{ "agent_id": "concrete_slab", "role": "foundation" }
```

**Correct:**
```json
{ "agent_id": "foundation", "role": "foundation" }
```

### Wall agents

Wall agents use directional names: `south_wall`, `north_wall`, `east_wall`, `west_wall`.

For buildings with multiple wall panels on one face, the wall agent still represents the full assembled wall — panel splitting happens inside the component, not as separate agents.

### Roof agents

**Reuse the design spec's roof component ids VERBATIM.** The Placer pairs
agents to spec components by exact id — a renamed roof agent (spec
`south_roof` → agent `gable_roof_south`) silently keeps a stale placement and
the roof panel floats away from the walls. If (and only if) the spec has no
roof component to copy from, use descriptive names: `flat_roof`,
`mono_pitch_roof`, `gable_roof_south`, `gable_roof_north`.

---

## Agent Dependency Ordering

Dependencies define which agents must complete before others can start. Always declare:

```json
{
  "agent_id": "south_wall",
  "dependencies": ["foundation.dimensions"]
}
```

Standard ordering:
1. `foundation` — no dependencies
2. All four walls — depend on `foundation.dimensions`
3. Roof — depends on all four walls (e.g. `south_wall.top_plate`, `north_wall.top_plate`)

---

## Panel Size Limits — When to Split Agents vs. Split Panels

Standard SIP stock: **2440mm × 1220mm**. The 2440mm is the span limit; 1200mm is the standard panel width.

**Panels within a single wall/roof agent are always split** — a 6m wall uses five 1200mm panels joined by splines. This splitting happens inside the component code, not as separate agents.

**Agent splitting** is only needed if the design is inherently multi-section (e.g. two roof slopes for a duo-pitch). One wall = one agent regardless of length.

---

## MANDATORY: Design Spec `"roof"` Section

Every SIP design spec **MUST** include a top-level `"roof"` object. Without it the profile validator fires a warning on every generation and downstream component agents have no authoritative roof type.

| Roof type | `roof_type_key` | `roof_structure_key` |
|-----------|-----------------|----------------------|
| Flat / warm deck | `"flat"` | `"roof_structure/sip_flat_warm_roof"` |
| Mono-pitch | `"mono_pitch"` | `"roof_structure/sip_mono_pitch_panels"` |
| Duo-pitch gable | `"duo_pitch"` | `"roof_structure/sip_simple_gable_panels"` |

Example for a duo-pitch gable garden room:

```json
"roof": {
  "roof_type_key": "duo_pitch",
  "roof_structure_key": "roof_structure/sip_simple_gable_panels",
  "pitch_degrees": 30,
  "ridge_direction": "east_west"
}
```

**Non-negotiable:** A design spec with no `roof` section will always produce a validation warning. Include the `roof` object and always include `pitch_degrees` — read it from the brief; default to `pitch_degrees: 30` for duo-pitch if the brief does not specify, `pitch_degrees: 5` for mono-pitch, and omit `pitch_degrees` for flat. **`pitch_degrees` is critical** — it flows directly into the roof slope and gable wall component goals and is used verbatim by the builder. An incorrect default produces a visibly wrong roof angle.

---

## Coordinate System

All components share a common building coordinate system:

```
Origin (0, 0, 0) = SW corner of building footprint at Z = 0 (top of slab / floor level)
X-axis = East (along building width, BUILDING_WIDTH)
Y-axis = North (along building depth, BUILDING_DEPTH)
Z-axis = Up (vertical)
```

The foundation agent defines the global footprint. All wall and roof agents must use the same BUILDING_WIDTH, BUILDING_DEPTH, and PANEL_THICKNESS as the foundation.

**Z placement — foundation top at Z=0, walls sit ON Z=0:**

Z=0 is the **top of the slab** (floor level). The Foundation builder's local origin IS the
top face — geometry extends DOWN from local Z=0 to Z=−STRIP_DEPTH internally. So:

- ✅ foundation `position`: `{ "x": 0, "y": 0, "z": 0 }` (X/Y match the wall envelope's own origin — see Foundation goal above)
- ✅ every wall `position.z`: `0`
- ❌ foundation `position.z = -STRIP_DEPTH` — this DOUBLES the internal offset and buries
  the slab top at world Z=−STRIP_DEPTH, making the walls float above the foundation.

Key shared variables declared in the manifest (all agents must agree on these):

| Variable | Where set | Consumed by |
|---|---|---|
| `BUILDING_WIDTH` | foundation | all walls, roof |
| `BUILDING_DEPTH` | foundation | all walls, roof |
| `PANEL_THICKNESS` | design spec | all walls, roof positioning |
| `WALL_HEIGHT` | wall agents | roof eave height |
| `EAVE_HEIGHT` | derived | roof slopes |

---

## NON-NEGOTIABLE: Mandatory Key Values in Agent Goals

**The component agent receives only the `goal` string and its bounding box.** It has no access to the design spec. Every scalar value the component agent needs to generate correct geometry MUST appear verbatim in the `goal` field. Vague goals produce wrong geometry.

### Foundation goal — MUST include strip depth and panel thickness

**`BUILDING_WIDTH`/`BUILDING_DEPTH` are already the outer-to-outer wall
envelope** — eave walls span the full `BUILDING_WIDTH` with their outer face
at the building boundary (see "Eave (south/north) span" below). The
foundation's outer footprint is therefore `BUILDING_WIDTH × BUILDING_DEPTH`
**directly** — do NOT add `2*PANEL_THICKNESS`. Adding it produces a
foundation that projects a full wall-panel-thickness beyond the wall's own
outer face on every side, so the strip sits visibly outside the building
shell instead of directly under the wall panels (ISSUE-013).

Strip foundation goal template (substitute real values from the spec):

```
Create strip perimeter foundation: building footprint BUILDING_WIDTHmm × BUILDING_DEPTHmm,
strip depth STRIP_Dmm, strip width STRIP_Wmm.
Foundation outer footprint matches the wall envelope exactly — BUILDING_WIDTHmm × BUILDING_DEPTHmm.
Place at Vector(0, 0, -STRIP_D).
```

Example for 6000×4000mm building, 300mm deep strip:
```
Create strip perimeter foundation: 6000×4000mm footprint, strip depth 300mm, strip width 450mm.
Foundation outer footprint 6000×4000mm (matches wall envelope). Place at Vector(0, 0, -300).
```

### South/north wall goals — MUST include span, height, stock, and panel thickness

**Eave (south/north) span = `BUILDING_WIDTH`** (the FULL outer-to-outer building width). The eave walls are the roof-bearing walls and ALWAYS run through the corners outer-to-outer — never shorten them to a fit-between span.

```
Create south wall SIP panel: span BUILDING_WIDTHmm, height WALL_HEIGHTmm, stock SIP-XXX
(panel thickness PANEL_THICKNESSmm). Inner face at Y=0, outer face at Y=-PANEL_THICKNESS.
```

#### EXCEPTION — square footprint (`WIDTH == DEPTH`) on a FLAT roof

The through/butt rule needs a longer wall and a shorter wall. **When the footprint
is square there is no longer wall**, so the rule above does not apply and the
rectangle pattern (two walls at full length, two retreated at both ends) produces
geometry that does NOT close — the full-length pair overhangs the retreated pair by
one panel thickness at each end.

On a square, all four walls **pinwheel**: every wall retreats one panel thickness at
ONE end and runs through at the other, so each corner has exactly one owner.

**Every wall's span = `WIDTH - PANEL_THICKNESS`** — all four the same.

Worked example, `6000 × 6000`, `PT = 142`:

```
span = 6000 - 142 = 5858   ← south, north, east AND west, all four identical
```

```
Create south wall SIP panel: span 5858mm, height 2700mm, stock SIP-142
Create east wall SIP panel:  span 5858mm, height 2700mm, stock SIP-142
Create north wall SIP panel: span 5858mm, height 2700mm, stock SIP-142
Create west wall SIP panel:  span 5858mm, height 2700mm, stock SIP-142
```

Do NOT author `6000 / 5716 / 6000 / 5716` on a square — that is the rectangle
pattern and `sip.wall_continuity` will reject it as an overlap.

This exception applies to **flat** roofs only. A duo-pitch roof on a square footprint
still follows the eave-through rule below (the ridge direction, not relative length,
decides which pair runs through), so eaves span the full `WIDTH` and gables span
`DEPTH - 2*PT` exactly as for a non-square rectangle.

### Fall-run wall goals (flat / low-slope / mono-pitch) — MUST include BOTH end heights

A flat, low-slope (2–5°) or mono-pitch roof has **no ridge and no gable**. Its fall is
carried by the **wall tops**: the two walls running ALONG the fall direction are
**RAKED** — their top slopes in a single straight line from one end to the other, and
the roof deck rests on them and tilts with them.

For a south-to-north fall the fall-run walls are **east and west**. Compute:

```
RISE        = BUILDING_DEPTH * tan(FALL_DEG * pi / 180)
HIGH_Z      = EAVE_HEIGHT + RISE          # the high (south) end
LOW_Z       = EAVE_HEIGHT                 # the low (north) end
```

Then author the fall-run walls with **two heights** and the level end walls with one:

| Wall | `dimensions.height` | `dimensions.height_end` |
|---|---|---|
| east (fall-run) | `HIGH_Z` | `LOW_Z` |
| west (fall-run) | `LOW_Z` | `HIGH_Z` |
| south (high end, level) | `HIGH_Z` | omit |
| north (low end, level) | `LOW_Z` | omit |

Note east and west are **mirrored**: each starts at the end its own run begins at, so
one rises along its span and the other falls. Get this the wrong way round and the two
walls slope in the same direction, which cannot support a single plane.

**`dimensions.height_end` is REQUIRED on a fall-run wall.** It is the only channel that
carries the taper to the component agent — a wall with `height` alone builds as a
rectangular box, the deck then has nothing sloped to bear on, and it floats above the
wall tops (observed in the field repeatedly).

Worked example, `7000 x 5000`, `FALL_DEG = 2`, `EAVE_HEIGHT = 2700`:

```
RISE   = 5000 * tan(2°) = 174.6
HIGH_Z = 2874.6, LOW_Z = 2700
east_wall:  height 2874.6, height_end 2700
west_wall:  height 2700,   height_end 2874.6
south_wall: height 2874.6  (level)
north_wall: height 2700    (level)
```

Goal text for a fall-run wall MUST state both ends explicitly:

```
Create east wall SIP panel (FALL-RUN, RAKED TOP): span SPANmm, height HIGH_Zmm at the
south end raking to LOW_Zmm at the north end (height_end), stock SIP-XXX
(panel thickness PANEL_THICKNESSmm).
```

**Three things NOT to do:**

1. **Do NOT call it a gable wall.** A gable has a triangular apex at mid-span; a raked
   wall is a single straight slope. The gable builder fabricates an apex that does not
   exist on a flat roof.
2. **Do NOT attribute the fall to a tapered top plate, wedge, firring or tapered
   insulation.** The WALL is the taper. A separate plate double-counts the fall and
   appears as a stray part floating above the wall.
3. **Do NOT leave the roof deck flat-and-level in the description.** It is a flat panel
   that SITS on the raked tops, so it acquires the pitch from them.

### East/west gable end wall goals — MUST include eave height AND ridge height

For duo-pitch (gable) roofs the east and west walls have a triangular gable top. The goal MUST include both the eave height and the ridge height so the component agent can call `.gable()`.

**These two numbers are deterministic and owned by `Elixihub.Concept.RoofGeometry`** — `SipSpecNormalizer` OVERWRITES whatever you put in `dimensions.height`/`dimensions.eave_height` (and the `"gable"` sub-map, if present) with the formula result before the component agent ever sees the goal, so get them right here rather than relying on the correction:

```
gable_rake_end_h  (eave_height field) = WALL_HEIGHT + LIFT + PANEL_THICKNESS * tan(PITCH_DEGREES * π/180)
underside_ridge_z (ridge_height/apex) = WALL_HEIGHT + LIFT + (BUILDING_DEPTH/2) * tan(PITCH_DEGREES * π/180)
```

`LIFT = 90mm` (Fig 1.4.7 eaves joint: top plate 45mm + eave plate low edge 45mm) — the roof underside bears ABOVE the eave wall top by this amount. **Do not omit `+ LIFT`** — that omission is what let the ridge board and gable rake end up 90mm below the roof underside in a live project.

```
Create east gable end wall SIP panel: span (BUILDING_DEPTH - 2*PANEL_THICKNESS)mm,
eave height GABLE_EAVE_HEIGHTmm, ridge height RIDGE_Zmm (duo-pitch gable roof, symmetric apex),
stock SIP-XXX.
```

Do NOT tell the component agent to call `.orient("Y")` — the goal only needs
span/height/stock/ridge values. Pipeline wall scripts are expanded through
`Wall.construct()`, which always builds span-along-local-X regardless of the
wall's compass direction; the Placer's yaw sets world orientation. `.orient()`
is a `.build()`-only, non-pipeline API — see `sip_walls_elixifree.md`.

**Gable span = `BUILDING_DEPTH - 2*PANEL_THICKNESS`** (NOT the full `BUILDING_DEPTH`, NOT `BUILDING_DEPTH - PT`). Corner rule: the eave (south/north) walls run through the corners outer-to-outer, and the gable end walls BUTT between the eave walls' inner faces — the clear opening between those inner faces is `DEPTH - 2*PT`. This holds for every duo-pitch building regardless of which wall pair is longer. A full-depth gable span overlaps the eave wall bodies at both corners by PT. Embed the numeric value.

Compute both formulas and embed the numeric results. Do not leave either as a bare formula.

Example for 7000mm depth, 142mm panel, 2700mm eave, 30° pitch:
`span = 7000 - 2*142 = 6716` (butts between the eave inner faces),
`gable_rake_end_h = 2700 + 90 + 142 * tan(30°) ≈ 2871.99mm`,
`underside_ridge_z = 2700 + 90 + 3500 * tan(30°) ≈ 4810.71mm`:
```
Create east gable end wall SIP panel: span 6716mm, eave_height 2871.99mm, ridge height 4810.71mm
(duo-pitch gable roof, symmetric apex), stock SIP-120.
```

### Roof slope goals — MUST include eave height, pitch, building depth, and panel thickness

```
Create DIRECTION pitched roof SIP slope: building width BUILDING_WIDTHmm, half-span HALF_SPANmm
(= BUILDING_DEPTH/2), pitch PITCH_DEGREESdeg, eave height WALL_HEIGHTmm
(Z of top of eave wall), wall panel thickness PANEL_THICKNESSmm, building depth BUILDING_DEPTHmm,
stock SIP-XXX. is_south=True/False. eave_overhang 400mm, rake_overhang 300mm.
```

`HALF_SPAN = BUILDING_DEPTH / 2` — to the building centreline. **Never add PANEL_THICKNESS**;
the eave is placed at the wall outer face by the assembly layer, so both slopes meet on the
centreline. Adding `+ PT` clashes the two slopes by `2×PT` at the ridge. Embed the computed
numeric value; do not leave it as a formula.

**`pitch_degrees`, `stock`, `eave_overhang`, and `rake_overhang` must ALL be explicit** in the goal and passed directly to `PitchedRoofSlope(...)`. The builder defaults are 50mm for overhangs (far too small) and has no pitch default. The goal text will supply all four — read them verbatim.

The component script calls `slope.build()` and `result.add_to_doc("Body")`. The assembly layer
(Placer) applies pitch rotation from the design spec — do not set `feature.Placement` in the script.

Example for 8000×6000mm building, SIP-120 (142mm wall), 30° pitch, 2600mm eave, SIP-200 roof:
```
Create south pitched roof SIP slope: building width 8000mm, half-span 3000mm
(= 6000/2), pitch 30deg, eave height 2600mm, wall panel thickness 142mm,
building depth 6000mm, stock SIP-200. is_south=True. eave_overhang 400mm, rake_overhang 300mm.
```

### Ridge board component — REQUIRED for duo-pitch roofs (bevelled `RidgeBoard`, NOT a plain box)

A duo-pitch (gable) roof MUST include a `ridge_beam` component (agent id/role kept
as `ridge_beam` for backward compatibility — the geometry it builds is the
bevelled `RidgeBoard`, never a flat-topped `Part.makeBox`). The two slopes bear
on its two bevel faces at the apex; the arris (where the bevels meet) just
touches the panels' plumb-cut mitre from below. Omitting it leaves the ridge
unsupported and the board unplaced.

- `agent_id`: `ridge_beam`, role `ridge_beam`.
- Runs along the ridge (X), BUTTS between the gable walls' inner faces (same
  corner rule as every other component — it does NOT run outer-to-outer):
  `length = BUILDING_WIDTH - 2*PANEL_THICKNESS`.
- Cross-section: `width` (Y, across the ridge) = 90mm, `depth` (Z, overall,
  local top = arris) = `RIDGE_BOARD_D = max(200, ceil(HALF_SPAN/10))`.
- `dimensions`: `{ "length": BUILDING_WIDTH - 2*PANEL_THICKNESS, "width": 90, "height": RIDGE_BOARD_D }`.
- `position` (min-corner, starting hint only): `{ "x": PANEL_THICKNESS, "y": BUILDING_DEPTH/2 - 45, "z": RIDGE_Z - RIDGE_BOARD_D }`
  where `RIDGE_Z = EAVE_HEIGHT + LIFT + (BUILDING_DEPTH/2) * tan(PITCH)` (`LIFT = 90mm`,
  the Fig 1.4.7 eaves joint lift — do not omit it, see the gable end wall section above).
  The Placer (`Elixihub.Assembly.Placer`) deterministically snaps the board's
  final position so its ARRIS (not a flat top) lands exactly on
  `Elixihub.Concept.RoofGeometry.underside_ridge_z/4` regardless of what is
  declared here: `pos_z = underside_ridge_z - depth`.
- `connects_to`: the two roof slopes.
- The component script MUST use `RidgeBoard(length, width, depth, pitch_degrees)`
  — see `sip_roofs.md` "Ridge board MUST use `RidgeBoard`". A plain `Part.makeBox`
  ridge is a construction error (line contact only, not full-face bearing) and
  is auto-rewritten by the constructability layer to `RidgeBoard(...).construct()`
  when detected, but do not rely on that fallback.

Goal string:
```
Create ridge board: length (BUILDING_WIDTH - 2*PANEL_THICKNESS)mm along the ridge (X), 90mm wide (Y),
RIDGE_BOARD_Dmm deep (Z), double-bevelled top (arris on the centreline). Position centred on
building depth at the ridge line, arris at RIDGE_Zmm. LVL/glulam.
```

### Fastener agents — TEMPORARILY EXCLUDED (do not emit)

**Do NOT create fastener agents.** Anchor bolts, structural screws, hurricane
ties, spline screws and similar fixings are OUT OF SCOPE for the manifest right
now — omit them entirely rather than modelling them as agents.

Each fastener agent costs a full generate-verify-execute cycle for a part that is
standard hardware rather than bespoke geometry, which slowed every specification
without improving the building's envelope. They will be reintroduced by a
DETERMINISTIC feature that places them from the connection-details catalog, with
no LLM call per fastener.

Concretely:

- Do not emit agents named `*_anchor_bolts`, `*_screws`, `*_ties`, `*_fasteners`
  or similar.
- Do not add a fastener component to the design spec's `components` list.
- Do NOT compensate by folding a fastener into another component's
  `library_key` — a wall agent's `library_key` must still resolve to the wall
  itself, never to a bolt. (This was already forbidden and remains so: it is how
  a foundation once came out as an M12 anchor bolt.)
- Connection DETAIL in goal text is still fine and still wanted — describing a
  wall as "bears on foundation via sole plate + anchor bolts at 600mm centres"
  documents the junction without creating an agent for the bolt.

`sip.fasteners_present` (which used to reject a manifest with zero fastener
agents) is disabled in the SIP profiles while this holds.

### Interior partition components — span MUST be derived from `connects_to`, never the raw envelope

Interior partitions (`partition_*` agents, timber stud, not SIP) sit inside the building and are
listed in `connects_to` against their real neighbours (other partitions and/or perimeter walls).
**Their declared `dimensions` span is the distance between those two neighbours' faces — NEVER
`BUILDING_WIDTH` or `BUILDING_DEPTH`.** A partition's box must fit entirely inside the inner
envelope `[PT, BUILDING_WIDTH-PT] x [PT, BUILDING_DEPTH-PT]`.

`Elixihub.Concept.SipSpecNormalizer` re-derives (or, failing that, clamps) every partition's span
and position deterministically before component generation — but a badly authored span still
indicates the spec's `connects_to` graph disagrees with its declared geometry, so get it right
here.

**Worked example** (9000x7000mm building, PT=142mm — inner envelope Y in `[142, 6858]`):

A north-south hallway partition (`partition_hall_west`) running from the `partition_living_back_west`
partition's back face (y=3942) up to the north wall inner face (y=6858) has:
```
span = 6858 - 3942 = 2916mm   ✅ correct — derived from the real connects_to neighbours
position = { "x": 4142, "y": 3942, "z": 0 }
```

The observed field failure: the SAME partition declared with `span=7000` (the raw `BUILDING_DEPTH`,
not the neighbour distance) at `position.y=4042`:
```
far_end_y = 4042 + 7000 = 11042mm   ❌ wrong — 4184mm outside the north wall inner face (6858)
```
This happens when the span is copied from the building's overall depth instead of computed from
`connects_to`. Always compute span from the two real neighbour faces; if a neighbour cannot be
resolved, clamp the span so the partition's box stays inside the inner envelope rather than
emitting a full-envelope span.

### User-drawn partitions — `interior_walls` must reproduce verbatim, 1:1 with `partition_*`

When the context includes a USER-AUTHORED FLOOR-PLAN SKETCH block whose graph has `interior_walls`,
those are BINDING geometry, same as the perimeter. The emitted `building_graph.storeys[].interior_walls`
MUST reproduce them verbatim (same ids, endpoints, thickness, openings) — do not invent, drop, or
reposition them. Every `partition_*` component MUST correspond 1:1 with one `interior_wall` id: same
count, and each partition's position/span still follows the `connects_to`-derived rule above, applied
to the sketch's endpoints rather than a text description.

Example: sketch `interior_walls` has one entry `{"id": "s0_p0", "a": [4000,142], "b": [4000,5858],
"thickness": 100}` → the spec emits exactly one `partition_*` component, `connects_to` resolving to
the two walls/partitions at those endpoints, span `5858 - 142 = 5716mm` along Y at `x=4000`. A sketch
with two `interior_walls` and only one emitted `partition_*` (or vice versa) is wrong.
v0.0.985