- Add SAP-accuracy sample for uprn_10093116543 (epc.json, elmhurst_inputs.md, summary/worksheet PDFs) - Persist hyde viewer stack (xvfb/fluxbox/x11vnc/novnc/websockify) and Playwright chromium in the backend devcontainer; forward noVNC 6080 - Broaden .claude/settings.local.json allowlist (display/python/grep/tail) - In-progress campaign mapper/cert_to_inputs work carried from prior cert Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8 KiB
Handoff: build a full-SAP (SAP-Schema-17.1) → EpcPropertyData mapper
You are picking up a focused piece of work in /workspaces/model (a Python
repo, branch feature/hyde_make_it_more_accurate_with_tests). Read this whole
prompt before starting.
Goal
Add support for full-SAP certificates (schema SAP-Schema-17.1) to the EPC
mapper so a lodged full-SAP cert can flow through the existing chain:
EpcClientService.get_by_certificate_number(cert)
→ EpcPropertyDataMapper.from_api_response(raw) # <-- the work is here
→ EpcPropertyData
→ Sap10Calculator().calculate(epc) → SapResult
Today from_api_response raises ValueError: Unsupported EPC schema: 'SAP-Schema-17.1'. There is a captured real cert and a strict-xfail test
waiting to flip to green (see Acceptance).
Essential domain context (do not skip)
The GOV.UK EPB register holds two different certificate families:
- RdSAP (
RdSAP-Schema-*) — Reduced data SAP, for existing dwellings. The assessor records a reduced input set; the schema carries proxy/count fields (door_count,window,percent_draughtproofed,low_energy_fixed_lighting_outlets_count, …) and the mapper applies RdSAP table defaults downstream. - full SAP (
SAP-Schema-*) — the full SAP calculation input set, typically for new-build / on-construction assessments (assessment_type: "SAP"). It carries richer, measured geometry instead of proxies: explicitwindows,sap_opening_types,air_tightness,living_area,orientation,sap_ventilation,sap_flat_details.
All 7 currently-supported schemas are RdSAP. No full-SAP path exists anywhere yet. This is net-new, not a tweak.
EpcPropertyData (the target domain aggregate) and Sap10Calculator already
consume RdSAP-derived data fine — your job is the mapping, not the
calculator. Where full SAP gives a measured value that RdSAP only approximated,
prefer the measured value; where full SAP omits something RdSAP supplied,
you'll need a sensible default (mirror the RdSAP mapper's defaulting and
document each with an # ADR-...-style comment as the existing methods do).
Current state — exact files & line refs
- Dispatch:
datatypes/epc/domain/mapper.py,from_api_response(theif schema == "...":ladder, ~lines 2200–2261, ending in theraise ValueError(f"Unsupported EPC schema: {schema!r}")). Each branch doesfrom_dict(SchemaClass, data)(dacite) then afrom_<schema>static method. - RdSAP mapper methods: same file —
from_rdsap_schema_17_1(~line 623) is the closest analogue;from_rdsap_schema_18_0(~814) etc. Read 17.1 end-to-end first — it's your template. NoteAnyRdSapSchemaunion (~lines 98–105) and shared helpers_map_energy_elements,_map_energy_element,_api_sheltered_sides,_clear_basement_flag_when_system_built. - Schema models:
datatypes/epc/schema/— one dataclass module per schema (rdsap_schema_17_1.py, …). You'll addsap_schema_17_1.pyhere, modelling the full-SAP payload as frozen-ish dataclasses (from dataclasses import dataclass), reusingcommon.pytypes where shapes match.from_dict(dacite) builds it from the raw JSON, so the dataclass must match the JSON field names exactly (withOptional[...]for anything not always present). - Captured sample (your fixture + shape reference):
backend/epc_api/json_samples/real_life_examples/SAP-Schema-17.1/uprn_10092973954/epc.json(cert0862-3892-7875-2690-2325, lodgedenergy_rating_current= 83). - The waiting test:
tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py— theuprn_10092973954case is a strictxfail(raises=ValueError)flagged viaunsupported_schema=Trueon itsRealCertExpectation.
The structural delta (full-SAP 17.1 vs RdSAP 18.0 top-level keys)
Full-SAP has, RdSAP doesn't: windows (dict), sap_opening_types (list),
air_tightness (dict), sap_ventilation (dict), sap_flat_details (dict),
living_area, orientation, multiple_glazed_percentage, design_water_use,
data_type, sap_data_version, is_in_smoke_control_area,
seller_commission_report, assessment_date, address_line_2/3.
RdSAP has, full-SAP doesn't: window (singular proxy), door_count,
extensions_count, habitable_room_count, heated_room_count,
fixed_lighting_outlets_count, low_energy_fixed_lighting_outlets_count,
open_fireplaces_count, percent_draughtproofed, solar_water_heating,
mechanical_ventilation, glazed_area, glazing_gap,
multiple_glazed_proportion, pvc_window_frames, measurement_type,
insulated_door_count, suggested_improvements.
schema_version_original is LIG-17.0; assessment_type is SAP.
Map the rich full-SAP fields onto the EpcPropertyData / Sap* fields the
RdSAP path populates. Inspect the nested shapes (windows,
sap_opening_types, air_tightness, sap_ventilation) directly in the JSON
sample and against what Sap10Calculator/cert_to_inputs
(domain/sap10_calculator/rdsap/cert_to_inputs.py) actually reads.
Task
- Inspect the sample JSON thoroughly; enumerate every full-SAP field and nested object.
- Add
datatypes/epc/schema/sap_schema_17_1.py— dataclasses matching the full-SAP payload (dacite-compatible,Optionalwhere appropriate). - Add
EpcPropertyDataMapper.from_sap_schema_17_1(schema) -> EpcPropertyDatainmapper.py, modelled onfrom_rdsap_schema_17_1. Comment every place you default or reinterpret a field vs the RdSAP path. - Wire a
if schema == "SAP-Schema-17.1":branch intofrom_api_response(decide whether_clear_basement_flag_when_system_builtapplies — check what it does). - Flip the test: in
test_real_cert_sap_accuracy.pyremoveunsupported_schema=Truefrom theuprn_10092973954case and set itssap_scoreto whatever the calculator now produces — BUT first sanity-check that figure against the lodged rating (83) and, ideally, a domain expert. If the calc score diverges materially from lodged, that's a finding to report, not silently pin. Do NOT widen tolerances to force a pass.
Constraints
- Type safety: all new code must pass
pyrightundertypeCheckingMode = strict, zero errors. UseOptional[X]notX | None. Annotate all return types. No baredict— usedict[str, Any]for raw payloads. - TDD: this repo uses vertical-slice TDD (
/tddskill). Prefer one failing test → one impl increment → repeat. The end-to-end accuracy test is the outer loop; add focused mapper unit tests underdatatypes/epc/domain/tests/as you go (seetest_from_rdsap_schema.pyfor the pattern). - Domain language / ADRs: load-bearing mapping decisions (e.g. how full-SAP
windowscollapse onto the calculator's window model, howair_tightnessfeeds infiltration) should be recorded as ADRs indocs/adr/— the existing mapper methods referenceADR-0028etc. Use the/grill-with-docsskill if terminology is drifting. - Run the suite with:
python -m pytest tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py -q --no-cov -rxs
Acceptance criteria
from_api_responsemaps aSAP-Schema-17.1payload to a validEpcPropertyDatawith noValueError.Sap10Calculator().calculate(epc)runs end-to-end on cert0862-3892-7875-2690-2325and produces a SAP score.- The
uprn_10092973954accuracy case is no longer xfail — it's a real pin, with its score reconciled against the lodged 83 (divergence explained if any). pyright --strictclean; new mapper unit tests cover the full-SAP field mappings; no tolerance widening anywhere.
Capturing more full-SAP samples
scripts/fetch_real_life_epc_sample.py <uprn> resolves a UPRN to its latest
cert, writes it under
backend/epc_api/json_samples/real_life_examples/<schema_type>/uprn_<uprn>/epc.json,
and prints schema + lodged rating + current calc output (or NOT MAPPABLE).
Use it to gather a small full-SAP cohort to harden the mapper against shape
variation before pinning scores.