Model/docs/adr/0036-non-separated-conservatory-mapper-split-and-round-trip-persistence.md
Khalim Conn-Kowlessar 1a059c75bb Document non-separated conservatory handling (terms, ADR, migration) 🟩
CONTEXT.md gains Non-Separated / Separated Conservatory glossary terms; ADR-0036
records the mapper-owned §6.1 split + 1:1 persistence + round-trip contract; the
migration note lists the five new epc_property columns for the FE schema repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:56:05 +00:00

4.4 KiB

Status
accepted

Non-separated conservatory: mapper-owned §6.1 split, persisted 1:1, with round-trip fidelity as the contract

Decided in a /grill-with-docs session (2026-06-24) while fixing the gov-API conservatory path (repro cert 718138, RdSAP-Schema-19.0). Extends ADR-0015 (mappers own cert normalization) and echoes ADR-0035 (coherence is a synthesis-time obligation, never a calculator normalisation).

Context

The gov register lodges a Non-Separated Conservatory (conservatory_type = 4) as a glazed "building part" carrying only {floor_area, room_height, double_glazed, glazed_perimeter} — no construction age band, no wall/roof, no floor dimensions. Treated as a generic fabric building part it is mis-scored (its floor heat-loss is counted as a normal part) and cannot persist (the epc_building_part columns construction_age_band / wall_construction are NOT-NULL). The correct RdSAP treatment folds it into the dwelling under §6.1; a Separated Conservatory (type 2/3) is disregarded under §6.2 and lodges no glazed part. See CONTEXT.md for both terms.

Two placements were available for the §6.1 "split" (detect the glazed part, lift it out of the fabric parts, carry it as the dwelling's conservatory):

  • in the mapper, producing a normalized EpcPropertyData with sap_conservatory set and the glazed part excluded from sap_building_parts (the existing 21.0.1 choice, commit d501535c);
  • at the calculator / persistence-reload boundary, re-deriving the split each time the picture is scored.

The production flow is map → save → reload-from-DB → score. The mapper split alone is invisible to the scored picture, because the reload (_compose) did not reconstruct sap_conservatory and the glazed part was excluded from the persisted sap_building_parts — so the conservatory was silently dropped on the path that actually scores. This is the same latent gap the 21.0.1 fix carried.

Decision

  1. The §6.1 split lives in the mapper. It is cert-shape knowledge, not physics (ADR-0015). EpcPropertyDatasap_conservatory populated, glazed part excluded from sap_building_parts — is the canonical normalized shape. We do not re-derive the split in the calculator or at reload; a global coherence pass over the assembled picture is rejected for the same reason ADR-0035 rejected one for heating.
  2. Persist the conservatory 1:1 on epc_property. SapConservatory is a single optional value per dwelling, so it is five nullable columns on epc_property (alongside the existing scalar conservatory flags), not a child table. Save writes them; _compose rebuilds SapConservatory from them. The columns are FE/Drizzle-owned — documented in docs/migrations/epc-property-round-trip-fidelity.md, applied there, never by this repo.
  3. Round-trip fidelity is the contract that makes the split correct. Because the split lives only in the mapper, the only thing guaranteeing the scored (reloaded) picture matches the mapped one is that EpcPropertyData survives save → get unchanged. We treat that as an enforced invariant: the deep-equality round-trip test (Model#1129) gains a non-separated-conservatory case, and a structural field-coverage guard fails if any EpcPropertyData field is neither reconstructed by _compose nor on a documented allow-list — turning "latent until a fixture exercises it" into an explicit decision.

Consequences

  • A future reader sees a conservatory — a "building part" in the source — persisted as flat columns on epc_property and absent from the dwelling's building parts. That is deliberate: it is a §6.1 dwelling attribute, not a fabric part.
  • The persistence + guard are schema-version-agnostic, so they close the same latent gap for the already-merged 21.0.1 path, not only 19.0.
  • The structural guard documents sap_roof_windows and the solar_hw_collector_* fields as known round-trip gaps (their FE columns are tracked follow-ups), so they are visible rather than silently dropped.
  • The per-schema mapper piece (declare the four glazed fields + filter + fold) still has to be ported to the remaining gov mappers (17.0/17.1/18.0/20.0.0/ 21.0.0) one validated cert at a time; this ADR covers the shared shape, not that rollout.