Three corrections found by re-running property 742003 end-to-end:
- roofSegmentStats are POSITIONAL — real responses omit the segmentIndex field
the fixture happened to carry; key the centre/area lookup by array position.
- Base the cap on ground_floor_area (the footprint the roof covers), not the
greatest per-storey area; roof_area is the fallback.
- Clamp the basis by total_floor_area: predicted EPCs borrow the structural
template's geometry (742003: a 118.62 m² MAIN ground floor) decoupled from
the predicted 55 m² (ADR-0029), so without the clamp the cap reads the
template's larger footprint.
Result: 742003 plan A/92.4 (16 kWp) -> C/74.4 (6.4 kWp). 29 solar tests +
orchestration threading + products green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
select_conservative_configs now also caps panels by the dwelling's own usable
roof — min(0.70 × maxArrayPanelsCount, roof_area/cos(pitch) × 0.5 / panel_area)
— threaded from recommend_solar via roof_area(epc, MAIN) (ADR-0038). No-op on
correctly-matched homes; falls back to the Google cap when the EPC has no MAIN
part. Defeats Google footprint conflation (semi-detached/terraced).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
select_conservative_configs must accept the dwelling's roof area and cap panels
to its usable roof (ADR-0038) — bounding a 55m² dwelling to ~16 panels under
Google footprint conflation, while staying a no-op on correctly-matched homes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enrich each SolarRoofSegment from roofSegmentStats (centre + areaMeters2, keyed
by segmentIndex) and read panelHeightMeters/panelWidthMeters onto SolarPotential
— the geometry the Dwelling-Roof Cap (ADR-0038) needs. All Optional; existing
projection + config-selection tests stay green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Dwelling-Roof Cap (ADR-0038) sizes by usable roof area and ranks segments
by distance from the dwelling, so the projection must carry each panel's
footprint and each segment's centre + area (from roofSegmentStats).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ADR-0026 sized PV at 0.70 x Google maxArrayPanelsCount, assuming Google's roof
is the dwelling's. Medium-imagery footprint conflation (semi-detached/terraced)
returns the whole building's roof -> oversized arrays (property 742003: 55m2
bungalow -> ~16 kWp -> effective E/49.7 -> A/92.4 on solar alone). ADR-0038
bounds the array to the dwelling's own usable roof (roof_area-based budget,
proximity segment selection anchored on the dwelling), subsuming the legacy
GoogleSolarApi guards the rewrite dropped. CONTEXT: add Footprint Conflation +
Dwelling-Roof Cap, extend Solar Potential (per-segment centre + area).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When refetch_epc=False and no stored lodged EPC exists, the handler no longer
falls back to a live EPC API call — it treats the property as EPC-less and
hands it to the prediction path. This keeps REFETCH_EPC (lodged path) and
REPREDICT_EPC (prediction path) cleanly independent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both passed no sap_ventilation= → EpcPropertyData default None, dropping the
whole §2 block so MEV/MVHR certs scored as natural. Mirror 21.0.1's block for
the only fields each schema lodges: mechanical_ventilation_kind (§2 MV-kind
dispatch) + sheltered_sides (§S5, from built_form). Neither schema lodges the
fan/flue/vent counts (RdSAP Table-5 age defaults), so those stay unset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
19.0 + 21.0.0 pass no sap_ventilation= at all → EpcPropertyData default None,
dropping the entire §2 block (sheltered_sides + mechanical_ventilation_kind).
Extend the MVHR mapper test to both and add a sheltered_sides regression,
mirroring 21.0.1 (remaining ADR-0037 ventilation follow-up).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full-SAP certs mapped property_type=None, so the hard cohort filter silently
excluded them as comparables. Correctly typing them admits real lodged EPCs as
donors — a ground-truth-method change (cf #1245). Net over the n=36 fixture: 16
components better, 4 worse, 6 unchanged; gains concentrated in the physical
characteristics full-SAP certs measure (window_count 3.83->1.69, building_parts,
total_window_area, floor_construction, construction_age_band, glazing, walls).
The 4 that fell are new-build-vs-old-stock service mismatch on 1-2 targets each
(heating/water fuel, cylinder insulation) + floor_area. Tighten 16, loosen 4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17.1/18.0/20.0.0 built SapVentilation(sheltered_sides=…) and dropped the cert's
mechanical_ventilation, mis-scoring MEV/MVHR dwellings as natural. Mirror 21.0.1's
_api_mechanical_ventilation_kind. Natural certs (code 0/5 → None) unchanged
(corpus 73.3%/0.774, mapper corpus green, 726605 SAP 68.058 unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures the calc-facing ventilation-type gap (older RdSAP mappers drop
mechanical_ventilation_kind), the FE-side Main Fuel display, the sweep survivor
clusters, and the predicted-property display path — all separate from ADR-0037.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures the grill-with-docs decisions for the full-SAP-17.1 mapper completion:
rebaseline-to-10.2 (restores Rebaselining trigger (a)), calc-affecting fields
coupled with the sap_version flip, and growing Elmhurst-anchored validation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Terraform computes SOLAR_MIN_REQUEST_INTERVAL_SECONDS = N / (0.8 * 600 / 60)
from var.maximum_concurrency (=4.0s at N=32) so the fleet width has one source
of truth and the per-container Solar pacing tracks it automatically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>