Wadi Docs

Wadi structural conventions (coding guidelines)

A house can be well-formed but structurally unsound: it passes the schema and the wall/roof geometry check, yet the building would not stand up — a floor floats in mid-air, a room is open to the weather, walls hover above a phantom slab. These are the coding conventions every Wadi house must follow.

They are formally defined here and enforced in code by the structural linter (editor/src/lint/structural.ts), which runs automatically:

  • in check.sh (and validate.mjs) — errors fail the check, warnings are printed but advisory;
  • in the DSL editor — the status pill shows the count and lists every finding in its hover tooltip, while still rendering the model so you can see the unsound part.

Each finding carries its convention id (C1, C2, …). Each convention is a self-contained module under editor/src/lint/constraints/ (its check + this doc + its example fixtures), and this file is generated from those modules (editor/scripts/gen-conventions-doc.mjs) — so the doc and the linter cannot drift. Add a rule by adding a constraint module and regenerating.


The vertical model (why C1 and C3 exist)

Floors stack in source order (floor 0 = the Plinth floor). The renderer places them like this (editor/src/three/coords.ts):

  • A floor's base elevation = the running sum of the previous floors' height only. wall_height and slab_thickness do not raise the next floor.
  • The plinth block is drawn to its own height. So the floor above sits at plinth-floor.height, while the plinth top is at plinth.height — they must be equal or the floor above floats/sinks by the difference. → C1
  • slab_thickness lifts a floor's walls within its band (wallZ = base + slab_thickness) — it is the deck the walls stand on. With no slab object there is no deck, so the walls float by that amount. → C3

height, wall_height, and slab_thickness are otherwise independent — the model enforces no relationship between them. These conventions add the few relationships that structural soundness does require.


C1 — The plinth floor's height must match the plinth block height · error

Statement. A floor that carries a plinth object (the Plinth floor) must set an explicit height, and that height must equal the plinth block's height.

Rationale. The floor above is stacked at plinth-floor.height; the plinth block rises to plinth.height. If they differ, the floor above floats above the plinth (floor.height > plinth.height) or sinks into it (<). If the floor height is omitted it silently defaults to 100, almost never the plinth height.

Fix.

floor 0 "Plinth" height 40 {          // == the plinth block height below
  ground name "Ground" at (0,0) size (500,500)
  plinth name "Plinth" at (…) size (…) height 40
}

(If the plinth block omits its own height, it follows the floor height and is consistent by construction — but set the floor height explicitly anyway, so the stack is not left to the default.)


C2 — A room must wall every exterior side · warning

Statement. A room shown with a partial walls list must still wall every side that faces outside (no room beyond it). Interior (shared) sides may be omitted — the neighbour's wall stands on the shared centreline.

Rationale. A room shows exactly the walls it declares; a bare room (no wall lines) is enclosed on all four sides. But the moment you add a wall line to hang a door or window, the room switches to a whitelist — every side you don't list is now a hole. An exterior hole leaves the room open to the weather. It is a warning, not an error, because an open exterior side is sometimes intentional (a verandah / open padvi).

Fix.

room Living at (x,y) size (w,l) {
  wall east west                       // plain exterior sides — enclosed
  wall south { door Main at 120 size (36,84) }
  wall north { window N1 at 100 size (60,50) sill 35 }
}

C3 — A floor with no slab must set slab_thickness to 0 · error

Statement. A floor that has wall/room objects but no floor_slab object must set slab_thickness 0.

Rationale. slab_thickness is the deck the floor's walls stand on (wallZ = base + slab_thickness). Its default is 8. With no slab object there is no deck, so every wall on the floor floats slab_thickness units above the floor base. Setting it to 0 puts the walls on the floor base; alternatively, model the deck by adding a slab.

Fix.

floor 1 "Ground" slab_thickness 0 {   // no slab modelled → walls sit on the base
  room Studio at (…) size (…) { … }
}

(This does not fire on a floor that carries no walls/rooms — e.g. a Plinth floor of just ground + plinth, or a roof-only top floor — where slab_thickness is harmless.)


C4 — A stacked floor's height should equal wall_height + slab_thickness · warning

Statement. A floor that carries a floor above it (and has walls/rooms) should set height = wall_height + slab_thickness.

Rationale. The next floor sits at base + height; this floor's walls stand on the deck and reach base + slab_thickness + wall_height. When height is larger, the floor above leaves a gap over the walls; when smaller, the walls poke through it. It is a warning — a deliberate gap is legitimate (a service plenum, a deep transfer beam) — but usually they should match.

Fix.

defaults { floor_height 116 wall_height 108 slab_thickness 8 }   // 108 + 8 = 116

(Skipped for the plinth floor — governed by C1 — and for the topmost floor, since nothing stacks on its walls.)


C5 — A staircase must land on a floor, not below ground · warning

Statement. A staircase's descent must not carry it below the ground plane (z < 0).

Rationale. Only a climb down (top-anchored) stair can fall below ground: you place it on the upper floor and it descends. Put it on the wrong floor, or give it too large a total_height, and the expanded flight lands below ground — it still draws in the 2D plans (which ignore Z) but is buried and invisible in 3D, with no other error. A climb up stair is anchored on its own floor and ascends, so it never trips this.

Fix.

Prefer climb up: put the stair on the lower floor it rises FROM and let it ascend.

floor 1 "Ground Floor" height 116 {
  slab at (…) size (…)
  staircase name "Stair" at (212, 64) step (7, 11, 44)   // `at` = the BOTTOM (this floor)
    direction south climb up                             // ascends to the floor above
}

(Or, if you must keep it climb down, move it up one floor or reduce total_height.)


C6 — Openings on the same wall must not overlap · error

Statement. Two openings (doors/windows) cut into the same physical wall must not overlap along it. This includes openings that belong to two different rooms sharing a boundary wall.

Rationale. Each opening is a boolean-subtract from the wall. Overlapping spans merge into one ragged hole (or fight over the same brick), which is never what you meant — and on a shared wall it silently punches a bigger gap than either room's plan shows.

Fix.

Offset or narrow one opening so the spans are disjoint. Openings are measured from the wall's start corner (offset = near edge; the opening occupies [offset, offset+width]).


C7 — Furniture items should not overlap · warning

Statement. Two furniture items whose plan footprints overlap are flagged — as a warning, because it is sometimes intentional (a rug under a table, a lamp on a desk, deliberately stacked pieces).

Rationale. More often it's a placement slip — two beds dropped on the same spot, or an anchored piece that reflowed into another when a room was resized. The footprint used is the item's rotated bounding box (yaw-aware), so it matches what the plan draws.

Fix.

Reposition one item, or ignore the warning if the overlap is deliberate.


C8 — Two abutting rooms need a partition between them · warning

Statement. Where two rooms share a boundary line and neither declares a wall on it, there is no partition between them.

Rationale. A bare room (no wall lines) is enclosed on all four sides, so two bare neighbours have two walls on their shared line. But once both rooms switch to partial walls lists and both omit the shared side, the centreline is left open — the rooms merge into one space with no divider. C2 only guards exterior sides; this is its interior counterpart. It is a warning because an intentional open-plan link (kitchen into living) is legitimate.

Fix.

Declare the wall on one of the two rooms (the neighbour's wall stands on the shared centreline, so one is enough):

room Kitchen at (…) size (…) { wall north south east }   // east = the shared line
room Living  at (…) size (…) { wall north south west }

C9 — A floor's slab_thickness should match its slab object's thickness · warning

Statement. When a floor carries a floor_slab object with an explicit thickness, that thickness should equal the floor's slab_thickness.

Rationale. The floor's slab_thickness is the deck the walls stand on (wallZ = base + slab_thickness); the slab object's own thickness is how thick the slab MESH is drawn. If they differ, the walls sit at the floor's slab_thickness while the slab top is at the object's thickness, so the walls float above or sink into the drawn deck. (A slab with no explicit thickness follows the floor's slab_thickness and is consistent by construction — this only fires when both are set and disagree.)

Fix.

Make them equal — most simply, drop the slab's explicit thickness so it follows the floor:

floor 1 "Ground" slab_thickness 8 {
  slab name "Deck" at (…) size (…)          // no thickness → uses 8
}

C10 — The roof should cover the rooms of the top occupied floor · warning

Statement. Every room on the top occupied floor should sit under a roof segment — no room left entirely uncovered.

Rationale. The roof's segments span a plan area (each segment's ridge line ± its width). A room on the top floor whose footprint does not overlap any roof segment has open sky above it — usually a roof that was sized to the wrong footprint, or a room added after the roof. (Only a completely uncovered room is flagged, so eave overhangs and partial coverage never false-warn; a house with no roof at all — a terrace — is not flagged.)

Fix.

Extend or add a roof segment to span the room, or reduce the room. Roof segments cover start → end along the ridge, width across it, so grow width/end (or the plot variables they derive from) until the room is under it.


SP1 — A spiral staircase's central pole must be smaller than its radius · error

Statement. A spiral_staircase's pole_radius must be less than its outer radius.

Rationale. The treads run from the central pole out to the outer radius. If the pole is as wide as (or wider than) the stair, there is no tread left to stand on — the geometry collapses.

Fix.

Reduce pole_radius below radius (a pole is typically a small fraction of the radius).


Running the checks

wadi-skill/architect/scripts/check.sh house.wdl
  • ✖ [C…] — a structural error; the check exits non-zero. Fix before you save/share.
  • ⚠ [C…] — a structural warning; advisory. Fix, or keep it if the open side is intentional.

In the DSL editor the same findings appear in the status pill (hover for the full list); the model still renders so you can see the problem.


Planned conventions (not yet enforced)

Documented so authors know they matter; not linted yet:

  • Interior partition gaps — where two rooms share a centreline and neither declares that wall, there is no partition between them. (C2 only covers exterior sides.)
  • Slab thickness ↔ slab object — when a floor does carry a floor_slab, its slab_thickness should match the slab's own thickness so walls sit on the real deck.
  • Roof footprint coverage — the roof segments should span the top occupied floor's footprint (no uncovered rooms).