Model/docs/adr/0032-landlord-override-epc-overlay.md
Jun-te Kim 1c3b4a7b72 Correct ADR-0032 dec-4: property_type affects party-wall calc + eligibility, not just metadata
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:07:30 +00:00

5.9 KiB
Raw Blame History

Landlord-Override EPC overlay (epc_with_overlay)

When a Property has a lodged EPC and resolved Landlord Overrides, the Effective EPC is the lodged EPC with those overrides applied as a simulation, so the SAP10 calculator scores a picture that reflects what the landlord knows beyond the cert (typically before a survey). This records how that overlay works and refines ADR-0031 decision 3, which deferred it ("Landlord Overrides overlay is a later slice"). Terms in CONTEXT.md (Effective EPC, Landlord Overrides, Rebaselining, Validation Cohort).

Status

Accepted (design). Refines ADR-0031 dec-3. Implementation: tracer slice (wall_type) first, coverage expanded per component.

Decisions

1. A Landlord Override is a Simulation Overlay, folded onto the lodged EPC

The overlay reuses the existing EpcSimulation / overlay_applicator machinery that every modelling Measure already uses to mutate an EpcPropertyData (domain/modelling/simulation.py). "The landlord says the wall is now insulated" and "a Measure insulates the wall" become one code path: a resolved override maps to an EpcSimulation, and effective_epc's epc_with_overlay branch folds it onto the lodged epc. CONTEXT.md already frames overrides as "a simulation on the EpcPropertyData" — this makes that literal. Rejected: bespoke field-writing onto a copied EpcPropertyData, which would duplicate the fold and invent a second way to mutate an EPC.

2. The Property's override slot holds domain overlays; the repository maps to them

Property gains a landlord-overrides slot typed as domain EpcSimulations, not the repository's ResolvedPropertyOverrides (which would make domain/ import repositories/ and invert the dependency). PropertyPostgresRepository hydrates the slot by reading the faithful value-space snapshot (PropertyOverridesReader) and mapping each override value → an EpcSimulation — the same place it already hydrates epc / predicted_epc.

3. The fold is partial and per-component

property_overrides is partial — only the components a portfolio actually supplied are present (a Property may carry property_type + wall_type but no roof_type). The overlay applies only the overlays derived from rows that exist; every other field stays lodged-as-is. This falls out of the faithful reader, which returns only the rows present.

4. WallType / RoofType decompose into SAP int codes — the calculator reads codes, not descriptions

The SAP calculator derives the wall U-value from the wall_construction and wall_insulation_type int codes, never from walls[].description (cert_to_inputs.py). So a wall_type override only moves the score once translated into those codes. The translation is tractable because a WallType value is material × insulation state: material → wall_construction (codes 1-5) and state → wall_insulation_type (1 external, 3 internal, 4 as-built, 7 filled), the code sets already documented in solid_wall_recommendation.py. The override's building_part maps directly to the overlay's BuildingPartIdentifier (0 → MAIN, 1 → EXTENSION_1, …). RoofType resolves only for the explicit "Pitched, N mm loft insulation" family → roof_insulation_thickness; roofs with no clean loft depth (flat, room-in-roof, "another premises above") produce no overlay.

property_type / built_form_type are whole-dwelling categorical corrections, not building-part fabric, so they set the top-level EpcSimulation.property_type / built_form (alongside the existing whole-dwelling lighting/heating overlays), folded by apply_simulations. They are written as the landlord text value ("House", "Park home", "Semi-Detached") — property_type consumers tolerate text and the calculator's park-home check is text-only. Correction to the first draft of this decision: property_type is not metadata — it drives party-wall heat loss (heat_transmission.py _is_flat_or_maisonette) and ASHP/solar/wall eligibility, so a correction moves both the SAP score and the measure menu. built_form has no calculator consumer today (it feeds the ML transform + reporting), so its overlay is currently inert at the SAP layer but kept for picture-completeness.

The "(assumed) insulated" / "partial insulation (assumed)" WallType states are deferred: their wall_insulation_type is age-inferred in RdSAP, so the code is ambiguous and must be pinned against the Elmhurst accuracy harness rather than guessed.

5. An overlaid Property is excluded from the Validation Cohort — on divergence, not source path

Producing a more accurate score than the cert is the point: the calculated Effective Performance deliberately diverges from the Lodged Performance (which is preserved). That divergence means an overlaid Property is no longer a clean calc(effective_epc)-vs-lodged control, so it is excluded from the Validation Cohort — exactly as a predicted-source Property is. The exclusion signal is divergence ("≥1 override was applied", the overlay slot is non-empty), not source_path: epc_with_overlay also covers plain-EPC Properties with no overrides, which are valid validation targets. This reuses the same divergence signal CONTEXT.md already names as a Rebaselining trigger. No Validation-Cohort code path exists today, so this is a recorded rule, not code in this slice.

Consequences

  • EpcPropertyData is untouched — provenance stays structural on the Property (ADR-0031 dec-3), and every downstream consumer is oblivious to the overlay.
  • The override→EpcSimulation mapping is the real deliverable and grows per component; wall_type is the tracer (highest SAP impact, existing code-set prior art). roof_type and full WallType coverage follow.
  • Precedence is unchanged (CONTEXT.md): the overlay lives entirely inside the epc_with_overlay branch and never applies when Site Notes are the source.