# Roof insulation as an option

Ken (13 Sep 2026): "work on the roof insulation options." The thermal evaluation (`docs/engineering/thermal-evaluation.md`,
`docs/estimate/thermal-r-value.xlsx` Code and Options tabs) shows the roof is the one element that misses the energy
code under every k-value case: the house roof as cast is about R-20 effective with Ken's measured R-2.1/in (R-8.7 with
the literature value) against the Montana zone 6 ceiling target of U 0.026 (R-38.5), so it needs roughly 3.05" of
polyiso (4.9" with the literature value). The wall passes as a mass wall; the roof does not have that path. This makes the
insulation a priced, drawn-in-the-numbers option on every roof: house shed / flat / gable and the shower house.

## The spec

```js
spec.roofInsulation = { type: 'none' | 'polyiso' | 'eps' | 'mineralwool', inches: number }
// default { type: 'none', inches: 0 } — reproduces today's prices to the cent (the pins hold at 0 % O&P:
// shed/A 258,930.98 · flat/A 247,226.99 · gable/A 266,725.69 · shed/B-plant 286,814.37; shower reference 95,719.20)
```
`inches` in 0.5" steps, 0 to 8 (limits.js). `type: 'none'` forces 0; a type with 0" is the same as none.
`normalizeSpec` fills the default; an old share link without the key decodes to none.

## Where it goes in the build

Rigid board over the top pour, under the membrane: the Eco Finish membrane stays, but it goes on a cover board over
the insulation instead of on the concrete. Lines are SITE lines in the Roof section (the plant casts the strips the
same either way; nothing on the Manufacturing tab moves), after the top pour and before the membrane:

| line | qty | unit | basis key (all placeholders, yellow) |
|---|---|---|---|
| `<Type> roof insulation, <inches>"` | roof area × inches × (1 + waste) | sf·in | `insulPricePerSfIn[type]`: polyiso 0.65, eps 0.45, mineralwool 0.95 $/sf per inch; waste 5 % |
| Cover board over the insulation, 1/2" gypsum roof board | roof area | sf | `coverBoardPrice` 0.90 $/sf (only when inches > 0) |
| Insulation fasteners and adhesive | roof area | sf | `insulFastenPrice` 0.35 $/sf |
| Edge nailer and fascia extension for the added height | roof perimeter | lf | `insulEdgePerLf` 3.50 $/lf (only when inches > 0) |
| Labor: set the insulation and cover board | roof area × layers × `mhInsulLayer` | man-hours | 0.025 mh/sf per layer; layers = ceil(inches / 3) (boards come up to 3"–4") |

Roof area = the area the membrane line already uses (`R.areaSf`, both slopes on the gable). Perimeter = the roof edge
length the fascia trim uses (derive from the geometry the same way the trim band does; if the trim helper is not
reusable, `2 × (roof length + roof width)` at the roof edge including overhangs).

## The thermal number the option shows

The engine reports the roof's R and U so the customer sees what the option buys, with everything in the basis:

```js
basis.thermal = {
  concreteRPerIn: 2.1,   // Ken's measured value, 12 Sep 2026 (yellow; the literature value is 0.9 at 30 pcf / 0.75 at 35)
  // ONE factor per roof product — derived, not typed: each block's effective R in thermal-r-value.xlsx Roof tab
  // (measured k, parallel-path and isothermal-planes averaged on U) ÷ THIS basis's clear R. Dividing by the
  // engine's clear R is the film normalisation: the workbook's roof films are 0.61 ceiling + 0.17 out + 0.00
  // membrane = 0.78, the engine's filmsR is 0.85, so this convention is what makes rClear × factor land on the
  // workbook's effective R (house 29.20 × 0.690 = 20.148 vs 20.141; shower 25.00 × 0.756 = 18.900 vs 18.904).
  roofBridgingFactorHouse: 0.69,        // house roof strips, 800S162-68 (8" web, 68 mil) in the 13.5" / 15.5" stack
  roofBridgingFactorShowerhouse: 0.756, // shower-house roof, 600S162-54 (6" web, 54 mil) in the 11.5" stack — a smaller,
                                        // thinner stud in a thinner stack bridges less, so this reads HIGHER; replace both when C1363 tested
  filmsR: 0.85,           // inside 0.68 + outside 0.17
  boardRPerIn: { polyiso: 6.0, eps: 3.8, mineralwool: 4.2 }, // aged / LTTR values; use the product's LTTR once chosen
  // zone 6 is Montana's own number: ARM 24.301.161 amends the 2021 IECC Table R402.1.2 zone 6 row to ceiling U 0.026
  // (the model code says 0.024). Montana amends ONLY that row, so zones 5 and 7 keep the model-code 0.024.
  ceilingTargetU: { zone5: 0.024, zone6: 0.026, zone7: 0.024, commercialIEAD: 0.032 }, // commercial IEAD still VERIFY
  zone: 'zone6',
}
roofRClear = stackIn × concreteRPerIn + filmsR      // stackIn = the roof concrete stack (13.5" house shed/flat, 15.5" gable, 11.5" shower house) — read from the geometry / takeoff, do not hard-code
roofREffective = roofRClear × roofBridgingFactor(product) + inches × boardRPerIn[type]   // house | showerhouse picks the cell
roofU = 1 / roofREffective
facts.roofThermal = { rClear, rEffective, u, targetU, meets: u <= targetU, inchesToCode: max(0, (1/targetU − rClear × factor) / boardRPerIn[type]) }
```
`inchesToCode` for the selected type (polyiso when none) drives a "To code" button in the UI that sets the thickness
to the next 0.5" step at or above it.

## The configurator

On the Price tab (house and shower house), under the roof choice: a "Roof insulation" control with the type
(None, as cast · Polyiso · EPS · Mineral wool) and a thickness stepper (0.5" steps), a "To code (zone 6)" button, and a
one-line readout: "Roof R-20 (U 0.050) · short of the zone 6 ceiling value by R-18" or "Roof R-41 (U 0.024) · meets
the zone 6 ceiling value". Customer mode: same readout, "board insulation" instead of the product word if the jargon
check objects; no plant / margin language (none is involved). The cost table shows the new site lines in the Roof
section; the proposal mentions the insulation in the roof sentence when inches > 0. The 3D view does not change
(the added height is inside the fascia line; note it in the UI hint).

## The workbooks

`scripts/build-estimate.py` and `scripts/build-showerhouse-estimate.py`: two Summary selectors (Roof insulation type,
inches) and the five lines on the Roof tab as IF() formulas off them, zero at the defaults so the workbooks match the
engine as today; the Roof tab also computes R clear, R effective, U and the zone target from a Thermal block of yellow
inputs matching `basis.thermal`. `build-all-estimates.py` keeps verifying at the defaults; add one check that sets
polyiso 3.5" through Excel COM in a temp copy and compares the grand total to the engine at the same spec.

## Pins and tests

- Default = none reproduces every existing pin.
- polyiso 3.5" on the default house adds exactly the five lines (test the arithmetic against the basis).
- `facts.roofThermal` for the default house at the measured k: rEffective ≈ 20.1 and inchesToCode ≈ 3.05 (polyiso);
  with 3.0" polyiso, meets = false by a hair and 3.5" meets = true. Shower house as cast R-18.90 (U 0.0529) on its own
  bridging factor, 2.058" to the commercial target — the button's 2.5" meets it; the shower factor must stay ABOVE the
  house factor (less bridging), and the house numbers (20.148 / 0.0496 / 3.052) must not move when either is touched.
- `normalizeSpec` clamps: negative or > 8 inches, unknown type → none.
- Customer labels pass the jargon test.
