# Building model: styles, atoms and the law that assembles them (v3.1, after review passes 1 and 2)

8 Oct 2026, 00:30 CET. Follows "Houses from atoms" (spike asset-library-20261007, `browse/houses.html`).
Repo: `~/forge-build/forge-assets` (canonical copy gex44 `/srv/forge-assets/repo`). Today's assembler: `omg/kit/houses.mjs` (287 lines).

## 1. What the operator saw, and the root cause

| Glitch | Root cause in today's code |
|---|---|
| Upper windows smaller than ground ones | Two window atoms with different openings (0.95 x 1.30 ground, 0.85 x 1.10 upper); nothing says a storey's windows must match. |
| 2 small windows alone at the back | Back facade = a random 60 % subset of upper windows, no ground floor, no door. No facade logic at all. |
| Gutters pierce the roof | Gutter hung at `eaves line - 0.02` with no clearance check against the slate drip line; its 1 m pieces stop short of the corners. |
| Slates less polished than the baked house | 3 near-identical flat slates on a perfect grid, no jitter, no underlay, ridge/verge joints visible. |
| Everything granite and blue | One material shelf, one palette, one style, one building type. |

The missing piece is a **building model**: rules that decide what a building IS (storeys, bays, roles of each facade,
which opening goes where and at what size) before any atom is placed, plus a **checker** that rejects illogical results.

## 2. Four layers (each a separate file, pure data between them)

```
style pack (data)  ->  1. PROGRAM   ->  2. MASSING  ->  3. FACADES + ROOF  ->  4. LAYOUT       ->  5. RENDER
kit.glb + shelf        type + seed      volumes,        bay grid, opening      every atom as      three.js:
+ style.json           -> what rooms,   storeys,        per bay per storey,    {role, matrix},    instancing,
                       how many floors  roof form       roof furniture         wall panels        wall meshes, tints
                                                         \______ check() at every arrow: plain-sentence violations ______/
```

- `omg/kit/building/spec.mjs`: layers 1-3. `(style, type, seed, overrides) -> BuildingSpec`. Pure JS, no THREE, deterministic.
- `omg/kit/building/check.mjs`: `check(spec, style) -> [{rule, severity, where, sentence}]`. Errors make the generator re-roll
  the failing layer (max 8 tries, then the safest template); warnings are kept in `spec.findings`.
- `omg/kit/building/layout.mjs`: layer 4. `(spec, styleKit) -> { instances: [{role, atom, m:[16]}], panels: [...] }`. Pure JS.
  Roof courses, quoins, gutters, verges all live here (moved out of `houses.mjs`), so they are testable without a GPU.
- `omg/kit/building/render.mjs`: layer 5. `(layouts[], kit, THREE) -> group`. One InstancedMesh per atom part for the whole
  village (as today), wall panels per wall kind, per-instance colour tints.
- `omg/kit/building/village.mjs`: picks a coherent set of buildings for a place (section 6).
- `omg/kit/houses.mjs` stays as a thin compatibility wrapper (`breton(seed)` keeps working).

## 3. The style pack (one per style, `kits/<style>/`)

| File | Holds |
|---|---|
| `kit.glb` | Atoms, one named node each (as today). Authored by Astra in Blender. |
| `kit.json` | `forge.atom-kit.v2`: per atom its ROLE, opening size, sockets, bbox, tris, clearances (section 5). |
| `materials.json` + `materials/*.ktx2` | The shelf. Now with VARIANTS per slot (section 6). |
| `style.json` | The style law: numbers a builder obeys (below). Written by us from Astra's kit + references, reviewed. |

`style.json` (Breton example, numbers are the law, not suggestions):
```json
{ "style": "breton", "storey": {"ground": 2.75, "upper": 2.60, "attic_kneewall": 0.6, "floor_above_grade": 0.20},
  "plinth": 0.0, "wall": {"kind": "rubble", "thickness": 0.38},
  "bay": {"plain": 2.3, "shutters": 2.5, "max": 2.8, "end_pier_min": 1.0, "pier_min_between_surrounds": 0.35},
  "openings": {"window": {"w": 0.95, "h": 1.30, "sill": 0.80, "role": "window.std"},
               "door": {"w": 1.0, "h": 2.1, "role": "door.main"}, "back_door": {"w": 0.9, "h": 2.1, "role": "door.back"},
               "gable": {"w": 0.5, "h": 0.7, "role": "window.small"}},
  "head_line": "all windows of a storey share one head height = door head on the ground floor",
  "roof": {"forms": ["gable"], "pitch": [44, 44], "overhang": 0.25, "verge": 0.12, "cover": "slate", "dormers": "on bay axes only"},
  "types": ["house", "longhouse", "tavern", "shop", "chapel"],
  "palettes": {...} }
```

### Atom roles (style-agnostic names the layout uses; each kit maps roles to its nodes)
`window.std[.shutters]`, `window.small`, `window.wide` (shop), `door.main`, `door.back`, `door.barn`, `shopfront`,
`roof.unit.{a,b,c,d}` (slate/tile/shingle), `roof.ridge`, `roof.hip`, `roof.verge`, `roof.underlay`, `eaves.fascia`,
`eaves.gutter`, `eaves.gutter_end`, `eaves.bracket`, `downpipe`, `downpipe.shoe`, `corner.{long,short}`, `chimney`,
`dormer`, `sign` (tavern/shop), `lantern`, `string_course`, `beam.{post,rail,brace}` (timber frame), `jetty.joist`,
`parapet.coping`, `balcony`, `canopy`.
A style may lack a role; the type templates ask only for roles the style declares, and `check()` reports a missing role
as an error at load time, never at runtime.

## 4. The law (v2: numbers reconciled with the kit; spec rules and layout rules separated)

**Program -> massing (this round: one rectangular volume + optional lean-to, gable roof only)**
- A TYPE is a template: bays range, storeys range, depth range, ground use, required roles. Breton this round:
  house (2-4 bays, 1-2 storeys), longhouse (4-6 bays, 1 storey + habitable attic with dormers), tavern (3-5 bays,
  2 storeys, door_double + sign + 2 chimneys), shop (2-3 bays, window_shop on the ground floor).
- Width = 2 end piers + bays x bay width: the footprint follows the facade.
- Storeys are FLOOR-TO-FLOOR: ground 2.75 (floor 0.20 above grade: one step at each door), upper 2.60. Eaves =
  last full storey top, or + kneewall 0.6 when the attic is habitable. Upper storey never below 2.5 with a 1.30 window.
- Lean-to annex (optional, back or gable side): every bay it covers is blank below its roof line + 0.3; no downpipe at a
  covered corner.

**Spec rules (`check(spec)`, pure numbers, before any atom exists)**
1. Bay grid per facade; bay width 2.3 plain, 2.5 with shutters (measured between SURROUNDS and open shutter leaves,
   not clear openings); end pier measured from the outermost surround or open leaf edge to the corner >= 0.55 (quoin
   chain reaches 0.49 m in): with kit v1 shutters that is >= 1.19 from the clear opening, plain >= 1.0.
2. Axes stack: an opening on an upper storey sits on an axis of the storey below. Solid wall under a window is allowed
   (Astra: normal), a window over a door bay is allowed.
3. Breton default: every full storey uses the SAME window (`window.std`, 0.95 x 1.30). Smaller windows only in gables,
   dormers and attics. (A style may declare its own per-storey windows; modern keeps one HEAD line per storey.)
4. Head line: ground windows: sill 0.80 -> head 2.10 = door head. Upper sills at storey line + 0.80.
5. Facade roles: `street` (door + full grid, door on the centre axis when the bay count is odd); `garden` (SAME axes as
   the street facade mirrored (u_back = -u_front), a back door on one ground bay, windows on 1+ axes per storey,
   never a lone small window); `gable` (no axis window when a chimney sits on that gable: two flanking smalls allowed;
   otherwise 0-1 small window on the axis at attic level); `party` (blank).
6. Glazing ratio checked per role: street 0.10-0.30, garden 0.05-0.30; gables and party walls exempt.
7. Chimney top >= ridge + 0.4; dormers only on bay axes, >= 0.6 m from a verge.
8b. Habitable attic: eaves = lastFullStorey.y + lastFullStorey.h + kneewall (kneewall 0 or 0.60, nothing else); attic
   light comes from dormers and gable smalls only, never a standard window in a kneewall.
8c. Back door on an OUTER axis when the facade has 2-3 bays (never between two mirrored windows).
A failing spec re-rolls its seed (max 8), then falls back to a template house that itself passes check() (tested);
fallback frequency is reported.

**Layout rules (`checkLayout(layout, kit)`, on placed atoms with tolerances, named relationships only)**
8. Roof laid FROM THE EAVES UP: starter course at the drip line (drip = 50 mm beyond the fascia), positive headlap:
   gauge = (unit length - headlap) / 2 = (0.42 - 0.075) / 2 = 0.1725 for slate; the last course is dropped when it
   would pass the ridge and the ridge cap covers the gap; if the remaining gap is larger than the cap's cover
   (0.185 m minus 0.03), add a short top course (units scaled on their length axis) so the cap always covers it;
   joints stagger by half a unit on alternate courses, with a HALF unit at the verge on staggered courses (kit v1: the
   end single scaled to X 0.5; kit v2: `roof.unit.half`); course lift along the roof normal = 0.02 + 0.014 x (course % 2),
   never cumulative; courses end flush on both verges (end units may be the cut
   variant); seeded jitter (yaw +-1 deg, slide +-6 mm) on singles only; dark underlay plane under the cover.
   Most of each course is laid with STRIPS (4 slates), singles at the ends.
9. Gutter law: the gutter top is 20-40 mm BELOW the lowest point of the PLACED starter course and its centre under the
   drip; brackets every 0.6 m on the fascia; it runs the full eaves length: 1 m pieces + one last piece stretched or
   shortened <= 15 % (UV retiled), end caps both ends, an outlet + downpipe at each end. Test: no gutter vertex inside
   any placed starter-course unit box, and no gap > 5 mm along the run.
10. Openings overlap the wall cut by >= 30 mm (kit v2 surround flange); no window surround or open shutter inside the
   quoin zone or another surround.
11. Roof units are excluded from generic overlap tests (laps are intended); generic tests cover gutters, dormers,
   chimneys, openings, signs. Allowed penetrations (named pairs): opening atom into its wall, chimney base into the roof
   (<= 0.6 m), dormer into the roof, gutter bracket into the fascia, downpipe bracket into the wall.
   Roof units are tested against dormers and chimneys: no unit centre inside a dormer's roof cut polygon or inside a
   chimney footprint + 0.05 m.
12. Tints: kit v2 tints its `_paint` materials only. Kit v1 adapter (style.json `paint_slots_v1`): Breton_blue_timber gets
   a multiplier in the blue-shade palette (value 0.75-1.0), Aged_ivory_frames a warm white-to-cream multiplier.

**Performance budget (gate, not a hope)**: kit v1 (measured, Fable): ~90k triangles per house, so the kit v1 pass mark is
<= 2.5M triangles and <= 150 render calls for 24 buildings, measured and reported. Kit v2 and later: a 24-building
village <= 600k triangles (strip = one slab with joints in the normal map <= 48 tris, window <= 600, shutter leaf <= 100;
the running Astra jobs were briefed at 240/3,000: their kits get a decimation pass if they miss it),
measured on gex44 and at a phone-sized viewport (390 x 844, pixel ratio 2, shadows 2048). That is NOT a phone: real phone
frame time and memory stay "unverified" until a named device runs it (target sustained 30 fps, p95 frame <= 33 ms). Far LOD = one textured plane per
roof slope beyond 60 m (P1 if time).

## 5. Kit contract v2 (what Astra delivers per style)
- One GLB, named nodes, world-scale UVs, no embedded images, shelf by material name (as v1).
- kit.json `forge.atom-kit.v2`: per atom `role`, `opening`, `sockets`, `clearance_box`, `tris`, `variants`, and the kit's
  `paint_slots`, roof unit sizes and headlap, gutter offsets (`drip_to_gutter_top`, `drip_to_gutter_centre_out`).
- Opening atoms carry reveals + an outer surround overlapping the cut by >= 30 mm. One window per full storey; shutters as a
  separate tintable atom.
- Roof: 4 singles + 2 strips (4 slates / 8 tiles) + underlay + ridge with bedding; per-unit tris <= 40, strips <= 240.
- Gutter system on one socket line: fascia, gutter, bracket, end cap, outlet, downpipe, shoe.
- Paint slots: neutral light painted textures (suffix `_paint`) tinted per instance; no tinting of coloured textures.
- Whole kit <= 45k tris; textures 1024 max, KTX2 at publish.

## 6. Diversity with coherence (`village.mjs`)
A village is ONE style + ONE village palette drawn once from the village seed; every building draws from that palette only.
- **Shelf variants** (new textures, paid once per village): wall stone 3 tones (grey granite, ochre granite, pink granite),
  render/plaster 2, roof 2 (blue slate, green-grey slate), timber 2. A village draws 2 wall tones, 1 roof, 1 timber.
- **Paint tints** (free): joinery, shutters and doors use a neutral painted texture tinted per instance
  (`InstancedMesh.setColorAt`). Palette per style (Breton: navy, sea green, oxblood, white, ochre). A village draws 3
  colours; a building takes 1 for joinery and 1 for the door.
- **Weathering**: per-building shade on walls (+-6 % value) via vertex colour on the generated walls.
- **Shape**: type mix (houses 70 %, 1 tavern per 10 buildings, 1-2 shops, 1 barn/longhouse), annex/wing/porch choices,
  2 eaves heights, gable or hip from the style list.
- **Coherence rules**: one roof material and one pitch band per village; neighbours never share both type and width;
  front facades face the street (input: a street polyline or the placements the game already has).
Cost: a village pays its style kit once (~30 MB GPU decoded today, ~4.5 MB download) + ~3 MB per extra shelf variant; each
building stays ~1-1.5 MB. Draw calls grow with materials used, not with buildings (today 50 for 12 houses).

## 7. Styles and the critical path
- **Critical path (gates this delivery): Breton building model on kit v1** (window_ground used on every storey, back
  facade logic, roof from the eaves with headlap, gutter law, jitter, underlay, tints in shades of the kit's blue).
- **In parallel, never blocking (Astra in Blender, 3 seats, 75 min each):** Breton kit v2 (fixes + variants + paint
  slots + strips), Medieval kit v1 (half-timber: code generates straight members on the window grid and the infill
  panels; atoms for braces, jetty bressummer/joists/brackets, leaded casements, plain-tile strips, sign), Modern kit v1
  (render/concrete/timber-cladding panels, flat roof with parapet + coping + rainwater heads, tall windows, sliders,
  balconies, canopies). Each lands in the page as soon as it passes its own kit check; its style.json + type templates
  are written in parallel so the code paths (timber frame generator, flat-roof parapet) exist when the kit arrives.
- Cut this round: hips, wings, chapels, apartment blocks over 3 storeys, street alignment, room programming, library cards.
- Repetition is allowed (Astra): neighbours MAY repeat; coherence comes from the village palette, not from forced difference.

## 8. Wiring
- Library: each style pack becomes a card `kit.building.<style>/<slug>` (versioned, release hash over kit.glb + shelf +
  style.json), indexed like every other asset; `building.*` lookups that hit a style return the kit + a type.
- OMG: `games_library.py` ships `building/*.mjs` + the style pack(s) the brief asks for; game code calls
  `village(style, seed, placements)` instead of loading the one baked house. Baked house stays as the fallback.
- Feedback/Re-run: each style pack tile gets the same feedback box; a re-run goes to Astra with the kit job.

## 9. Proof (gates before showing the operator)
- Unit: 200 seeds x every Breton type -> 0 spec errors after re-roll, fallback < 5 %; same seed = same hash; the rules
  for verticality, one window per storey, head line, garden facade and gutter each have a test that FAILS on today's
  `breton()` output.
- Layout: gutter law and roof coverage tested on 50 seeds without a GPU.
- GPU (gex44): village of 24 per available style: 0 console errors; budget numbers above; shots: street facade, back
  facade, eaves/gutter close-up, roof close-up, village wide, phone viewport.
- Look: the root compares the shots to the 5 complaints and says fixed / not fixed per complaint.

## 10. Fleet
| Seat | Model | Job | Files (own files only) |
|---|---|---|---|
| A1 | Astra (Blender) | Breton kit v2 | `jobs/breton-atom-kit-v2/` |
| A2 | Astra (Blender) | Medieval kit v1 | `jobs/medieval-atom-kit-v1/` |
| A3 | Astra (Blender) | Modern kit v1 | `jobs/modern-atom-kit-v1/` |
| S1 | Sonnet 5.5 | spec + check + Breton style.json/types + tests | `omg/kit/building/{spec,check}.mjs`, `kits-src/breton/style.json` |
| S2 | Sonnet 5.5 | layout + checkLayout + render (roof law, gutter law, panels, tints) + tests | `omg/kit/building/{layout,render}.mjs` |
| H1 | Haiku 5.5 | style.json for medieval + modern (from the kit briefs), village composer, page, GPU probe | `village.mjs`, `kits-src/{medieval,modern}/style.json`, `browse/buildings.html`, `tools/gpu/buildings_probe.py` |
Interfaces frozen in `omg/kit/building/TYPES.md`; S2 and H1 work against a fixture spec until S1 lands. Root integrates,
runs the gates, publishes the spike, reports at the 03:30 CET checkpoint.

## 11. Review pass 1 -> v2
| Finding (who) | Change |
|---|---|
| Scope too big for 2 h (all three) | Breton law on kit v1 is the only gate; 3 kits run in parallel, non-blocking; cut list in 7. |
| Head 2.15 vs door 2.10 (all three) | Sill 0.80 -> head 2.10. |
| Bay 2.0 vs shutters 2.23 m, piers vs surrounds, end pier vs quoins (Fable, Astra) | Bay 2.3 / 2.5 with shutters, measured between surrounds; end pier 1.0. |
| Slates overshoot the drip and pierce the gutter; zero headlap (Fable, Astra) | Rule 8: from the eaves, gauge 0.1725 with 75 mm headlap, last course under the cap; rule 9 tests placed units. |
| Gutter stops short / no outlets (Fable, Astra, Gemini) | 1 m pieces + one stretched <= 15 %, end caps, outlets, downpipes. |
| Wall cut leaks (Gemini) | Surround overlaps the cut by 30 mm (kit v2). |
| Per-slate/tile instancing on phones (all three) | Strips; triangle and call budget measured at a phone viewport. |
| Geometric checks need placed atoms; generic overlap rejects laps (Astra, Fable) | `check(spec)` vs `checkLayout(layout)`; named relationships; roof units excluded. |
| Tints on coloured textures make mud (Fable) | Shades of the kit blue now; neutral `_paint` textures in kit v2. |
| Gable window vs gable chimney; annex covers bays (Fable) | Rules 5 and the lean-to rule. |
| Storeys floor-to-floor, plinth step (Astra, Fable) | Ground 2.75 / upper 2.60 floor-to-floor, floor 0.20 above grade. |
| "No floating windows" wrong; forced neighbour difference wrong (Astra) | Rule 2 relaxed; repetition allowed. |
| Back facade: blank or mirror? (Gemini mirror, Fable/Astra sparse is fine) | Mirrored axes, back door, 1+ window axis per storey, no lone small window. |
| Hips need cut units (Fable, Astra) | Hips cut this round. |

## 12. Review pass 2 -> v3 (final; all three said "contract corrections, keep the parallel styles")
| Finding (who) | Change |
|---|---|
| Tint contract vs kit v1 (Astra) | Rule 12: `paint_slots_v1` adapter, test that two buildings get different blues. |
| Attic eaves defined twice; standard window in a kneewall (Astra, Gemini) | Rule 8b; longhouse fixture from S1. |
| Gutter remainder, fall, elbows (Astra, Gemini) | Rule 9: last piece cut short, 3 mm/m fall, swan-neck offset. |
| Ridge coverage after dropping a course; stagger (Astra) | Rule 8: short top course when needed; half-unit stagger. |
| Fallback not validated; allowed penetrations (Astra) | Fallback passes check (tested); named pairs in rule 11. |
| Back door between mirrored windows on 2-3 bays (Gemini) | Rule 8c. |
| Phone viewport is not a phone (Astra) | Labelled unverified until a named device. |
| Kit v1 wall cuts vs upper windows (Gemini) | No change needed: v1 `window_ground` is now used on every storey and the cut = its own opening. |
| Gate 600k unreachable on kit v1 (~90k per house) (Fable) | Kit v1 pass mark 2.5M / 150 calls, measured; 600k for v2 with slab strips. |
| Gutter piece arithmetic (Fable) | n = round(L), each L / n. |
| Fixture shutters in the quoin zone (Fable) | End pier from the leaf/surround edge >= 0.55; fixture widened to 8.33 m. |
| Garden glazing floor vs 1 axis per storey (Fable) | Garden 0.05-0.30. |
| course_lift ambiguous (Fable) | 0.02 + 0.014 x (course % 2), never cumulative. |
| No half slate at staggered verges (Fable) | v1: end single scaled X 0.5; v2: roof.unit.half. |
| Slates through dormers/chimneys untested (Fable) | Rule 11 test. |
| Four downpipes on a cottage (Fable) | One per run, two past 8 m, not beside the door. |
| Haiku seat overloaded (Fable) | Root runs the GPU probe and integrates the page; H1 keeps data, village, page draft. |
