# Building model: frozen interfaces (8 Oct 2026)

Blueprint: dashboard docs `FORGE-BUILDING-MODEL-BLUEPRINT-2026-10-08.md`. All numbers in metres, degrees in the JSON,
radians never leave a module. Building frame: origin on the ground at the centre of the MAIN volume's footprint, Y up,
+Z = the street (front) facade's outward normal, X along the front facade (to the right seen from the street).

## style.json  (`kits/<style>/style.json`, data only)
```js
{ style: 'breton', version: 1,
  roles: { 'window.std': 'window_ground', 'window.std.shutters': 'window_ground_shutters', 'door.main': 'door', ... },
        // role -> kit.json node name, or an array of variants picked by seed
  storey: { ground: 2.75, upper: 2.60, attic_kneewall: 0.6, floor_above_grade: 0.20 },   // floor-to-floor,
  wall: { kind: 'rubble', thickness: 0.38, material: 'Granite_lime_moss' },
  bay: { plain: 2.3, shutters: 2.5, max: 2.8, end_pier_from_edge: 0.55, pier_min_between_surrounds: 0.35 },
  openings: { window: { role: 'window.std', w: 0.95, h: 1.30, sill: 0.80 }, door: { role: 'door.main', w: 1.0, h: 2.1 },
              back_door: { role: 'door.back', w: 0.9, h: 2.1 }, gable: { role: 'window.small', w: 0.5, h: 0.7 } },
  ratios: { window_h_over_w: [1.25, 1.7], glazing: { street: [0.10, 0.30], garden: [0.05, 0.30] } },,
  roof: { forms: ['gable'], pitch: [44, 44], overhang: 0.25, verge: 0.12, cover: 'slate', drip: 0.05,
          unit: { w: 0.345, l: 0.42, headlap: 0.075, gauge: 0.1725, strip: 4 } },
          // course lift along the roof normal = 0.02 + 0.014 * (course % 2): NEVER cumulative,
  types: { house: {...TypeTemplate}, tavern: {...}, ... },
  palette: { wall: ['Granite_lime_moss', ...], roof: ['Blue_grey_slate'], timber: [...], paint: ['#1f3a5f', ...] },
  paint_slots_v1: { Breton_blue_timber: 'shutters', Aged_ivory_frames: 'joinery' } }   // kit v1 tint adapter (rule 12)
```
TypeTemplate: `{ bays: [min,max], storeys: [min,max], depth: [min,max], ground: 'house'|'shop'|'tavern'|'barn',
annex: ['none','lean-to'] , requires: ['sign', ...], chimneys: [min,max], dormers: 'none'|'bays'|'centre' }`.

## BuildingSpec  (`spec.mjs` output, `check.mjs` input; plain JSON, deterministic per (style, type, seed, overrides))
```js
{ style, type, seed, hash,                         // hash = fnv1a of the canonical JSON without hash/findings
  volumes: [ { id: 'main'|'annex', x, z, w, d,      // centre + size, w along X, d along Z (building frame)
               storeys: [{ y, h, use: 'ground'|'upper'|'attic' }], eaves, kneewall,   // eaves = last full storey y + h + kneewall (0 | 0.6)
               roof: { form: 'gable'|'lean-to'|'flat', pitch, ridge: 'x'|'z', overhang, verge, ridgeY },
               wall: { kind, material, thickness } } ],
  facades: [ { volume: 'main', side: 'front'|'back'|'left'|'right', role: 'street'|'garden'|'gable'|'party'|'hidden',
               len, axes: [u...],                   // bay axes, u = metres from the facade centre, right = + seen from outside.
               //   garden facade axes MIRROR the street ones: u_back = -u_front (same plan positions)
               openings: [ { kind: 'window'|'door'|'back_door'|'gable'|'shopfront', role, axis, storey, u, y, w, h,
                             shutters: bool } ] } ],
  dormers: [ { volume, side, u } ], chimneys: [ { volume, at: 'left'|'right'|'ridge', u } ],
  extras: [ { role: 'sign'|'lantern'|'bench', facade: 'front', u, y } ],
  look: { wall: 'Granite_lime_moss', roof: 'Blue_grey_slate', paint: { joinery: '#e9e2d0', door: '#1f3a5f', shutters: '#1f3a5f' }, shade: 0.97 },
  findings: [] }
```

Heights: every `y` in a spec is metres above GRADE (the building origin); an opening's `y` is the bottom of its CLEAR
opening (door y = ground floor level = storey 0 y), `storey` indexes `volume.storeys` (attic/gable = storeys.length).
Example: `fixtures/breton-house-3bay.spec.json` (hand-written, valid against the v2 numbers; S2 and H1 build on it until
spec.mjs lands).

## check(spec, style) -> Finding[]
Spec rules only (numbers, no atoms). `checkLayout(layout, kit)` (in layout.mjs) holds the geometric rules: gutter
clearance vs the PLACED starter course, gutter run gaps, roof coverage (no sky gaps, nothing past the ridge cap),
surrounds vs quoin zone, overlaps among gutters/dormers/chimneys/openings/signs (roof units excluded).
`{ rule: 'verticality'|'one-window-per-storey'|'head-line'|'garden-facade'|'pier'|'corner-zone'|'ratio'|'gutter-clear'|
'dormer-axis'|'overlap'|'missing-role', severity: 'error'|'warn', where: 'main/back/storey 1', sentence: 'plain words' }`
Pure function, no THREE. `check(layout)` variants for geometric rules live in `layout.mjs` as `checkLayout(layout, kit)`.

## Layout  (`layout.mjs` output, `render.mjs` input)
```js
{ instances: [ { role, atom, m: [16 numbers, column-major like THREE.Matrix4.elements], tint: '#rrggbb'|null } ],
  panels:    [ { kind: 'rubble'|'plaster'|'brick'|'render'|'cladding'|'infill', material, shade,
                 origin: [x,y,z], u: [x,y,z], v: [0,1,0], n: [x,y,z],   // u along the facade, n outward
                 outline: [[u,v], ...],                                  // polygon (rect or gable pentagon)
                 holes: [[u0, u1, v0, v1], ...] } ],
  bbox: { min: [x,y,z], max: [x,y,z] } }
```
Matrices are BUILDING-local; `render.mjs` multiplies by each placement's world matrix.

## render.mjs
`createBuildings(THREE, kit)` where kit = `await loadStyleKit(THREE, { base, renderer, GLTFLoader, KTX2Loader, basisPath })`;
`kit.build([{ layout, x, z, yaw }]) -> { group, stats: { buildings, instances, drawCalls, perBuildingBytes } }`.
One InstancedMesh per (atom part, material) for the whole batch; tints via `setColorAt` only on materials listed in the
kit's `paint_slots` (others get white).

## village.mjs
`village(style, seed, { count, plots? }) -> [{ spec, x, z, yaw }]` (plots = [{x, z, yaw, w, d}] from the game when it has
streets). Draws the village palette once (section 6 of the blueprint).

## Round 4 overrides (medieval houses, all optional)

`makeSpec(style, type, seed, o)` reads `archetype` (hovel, cottage, craft, burgher, patrician, tower), `wealth` (0..1),
`trade` (boot, pig, key, pretzel or null), `backDoor` and `backGrade` (metres relative to street grade). Existing `bays`,
`storeys`, `d`, `gableFront` and `plinth` still win. Absent overrides keep the previous shapes and pinned layouts.
A back grade within 0.35 m permits the floor-level back door; at -2 m or below it becomes `door.cellar` at that grade
and the stone base grows to reach it. Other back grades have no door. `backDoor: false` removes either back door.
Round-4 archetypes and explicit back-grade houses start at street grade; legacy defaults keep their 15 cm threshold.
The checker reports `door-grade` if a threshold stands more than 0.35 m above its actual ground.

`catalogue(style, seed, n = 48)` returns `{ archetype, type, o, w, d }[]`. Width/depth are the measured main footprint,
not the plot or trade-shed extent. The planner must rebuild using the same seed plus `o`; the catalogue is not a plan.
Additional optional overrides: `groundHeight`, `upperHeight`, `pitch`, `jetty`, `stoneStoreys` (0 = plain timber for
hovels/cottages), `bracePattern` (`none`, `k`, `x`) and `look` wall/timber/infill palette fields. Rich narrow gable fronts
have 2-3 bays; wide eaves fronts have 4-5 bays, three floors and a shallower roof to stay inside the triangle budget.

Optional spec fields: `archetype`, `wealth`, `backDoor`, `roles`; volume `backGrade`, `bracePattern`, `flatInfill`;
roof `cover: 'thatch'`; chimney/dormer `role`; extras `storey` and `roof` (ridge/apex attachment).
`kitInfo.kitAtoms` optionally maps kit names to arrays of atom names; pass the town-kit array to enable real thatch.
Cellar openings use `storey: -1`, existing `kind: 'back_door'|'window'` and roles `door.cellar`/`window.cellar`.
Town add-ons carry `kit: 'medieval-town'`, `box: [W,D,H]`, `boxZ` exactly like the trades kit; missing add-ons become
grey boxes with warnings. A missing thatch panel falls back to existing tile strips. Optional layout `findings` holds
those warnings. Flat plaster cells are existing `beam` panels with role `infill.cell` and 4 cm thickness, not a new
panel kind. Full-detail roofs cut strips at chimneys and verges instead of using many single tiles. Dormers stand on
the roof field (their 55-degree rear cut is sheared to match the chosen roof pitch) with their uphill cheeks buried in it; on a gable front they occupy the street end of a side slope.
No frozen field was renamed or removed. Lite/far detail and legacy full/lite layouts retain their previous rules.


## Round 5: optional house overrides

No required argument or frozen field changed. Medieval `makeSpec` also accepts:
- `w`: exact main-volume frontage. Bay counts change to keep each bay in 1.6-3.6 m, with wider workshop-door bays.
- `ground: { back, left, right }`: each value is `[min, max]` relative to street grade. Openings below `max + 0.25`
  are dropped, including doors. Remaining doors must meet the maximum ground within 0.35 m. Party walls ignore it.
  The common stone base extends to the lowest exposed wall ground. The front remains at street grade.
- `stepUp: 0..3`: floor and front threshold rise by 0.17 m per step; 0.3 m treads, door width plus 0.4 m.
- `irregular: 0..1`: zero is byte-identical to the previous spec. Seeded floor-height changes, window-axis jitter,
  continuous upper-floor shear and sagged tile/ridge placement use the existing topology and instances.

Optional volume fields are `stepUp`, `ground`, and `irregular: { amount, seed, sag, leanX, leanZ }`.
The upper-floor shear never changes a party wall's X plane. The street threshold and steps are not warped.
Unglazed windows have four 0.25 m deep wall-material reveals and a warm interior face exactly at their back edge.
These are existing beam/opening panels, not new panel kinds. Far-detail windows retain their previous flat treatment.

The shared round-4 checker has no step-top input: it still compares front thresholds to street zero and reports a
false door-grade error for three steps (0.51 m). Its owner must teach it the optional stepUp field. This worker does
not own check.mjs and does not hide that finding.

### Round 5 trades and barn

The new `style.types.barn` produces an 8-14 m frontage, 6-9 m depth, one tall plank-walled floor and a gable loft hatch.
It has a centred 3 x 3.5 m clear double door, paired vent slits, hay bale and cart, no chimney and no glazing.
Barn add-ons use `medieval-town-v2`; absent add-ons have sized grey boxes and findings. Planks fall back to
Oak_frame_grey until the aged shelf is available.

Optional `streetTrades: true` enables the street-facing forge/oven, smith's tools, soot-dark stone, bakery counter
and pretzel, wider tavern with two tables and barrels. It is also enabled by w, ground, stepUp or positive irregular.
Absent round-5 overrides preserve old trade specs and pinned hashes; irregular zero alone changes nothing.
These new extras use existing role/matrix/kit/box fields. They are catalogue display props, not collision-checked town
furniture. A settlement consumer must check their footprints and the free door path before placing them.

### Round 5 aged shelf

Optional `kitInfo.materials` is an array of material names available in the main renderer shelf. With v2 atoms and
that shelf, layouts choose aged strips/ridge, aged oak/plaster and Barn_plank. Without it, panels keep safe old
materials. Merge the v2 shelf into the main kit before supplying the array; houses.html does this. The separate town
kit's Thatch_reed material can be aliased to the v2 Thatch_reed_light shelf entry; the page demonstrates that alias.
Exposed wall feet get grime strips, with gaps at openings. Optional layout `meta.roofTint` and instance `roofTint`
are seeded small RGB multipliers, also carried by the existing tint field. The page applies them to roof instances.
The shared renderer currently ignores non-paint-slot tints, so a settlement consumer needs the same explicit roof
colour application or the renderer owner's support. Aged-kit triangle counts cannot be verified before that kit lands.

### Round 5 evidence and integration limits

Disposable viewer only: local loopback 8987 and GPU loopback 8988. Viewed the catalogue, unglazed window at three
angles, every trade at 30 m and 8 m, the same-seed irregular row and the back-ground [1.5, 2.5] case. Screenshots and
GPU stats are in `/tmp/w-houses5-shots/`. The probe reported no page errors or house-check errors, and clicked a real
view button. Missing v2 atoms are visibly grey boxes, not claimed finished barn doors, carts or tavern furniture.
The aged kit was absent, so aged textures, the light thatch and the aged-kit triangle total remain unverified.
The current-kit triangle estimate including street fittings peaks at 1.059x the equivalent old massing, below 1.15x.

`houses.test.mjs`: 20 passing tests. Full suite: 164 pass, 2 fail. The two failures are outside this worker's ownership:
- `lod.test.mjs` line 93 loops every type, but its pinned hash table has no barn entries. Existing type pins pass.
- `styles.test.mjs` line 143 insists every non-hall has stone ground, a 0.4 m jetty, multiple floors and 55-degree roof.
  A one-floor plank barn intentionally has none of those. Exempt barn there and test it with the new house tests.
No old test was weakened or skipped. A separate fitted-plot test initially exposed missing barn archetype metadata;
that was fixed and all nine layout-model tests pass. Barns retain optional archetype/wealth/back-grade metadata and
accept legacy bay-count fitting as well as exact w. Very narrow barns drop front vent pairs when they cannot clear
the great door; the normal 8-14 m type retains them.

The settlement owner still needs to merge the aged shelf, apply explicit roof multipliers, check front fitting
footprints and recognize step tops in check.mjs. Settlement modules, the shared renderer and kit assets were not
edited. No push, deploy or live-app test. Undo by reverting this worker's commits, not by copying a mirror tree.
