From e8212544edb44fc9f5c1cf1772eb6ff38eb6a014 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 4 Jun 2026 18:26:21 +0000 Subject: [PATCH] docs(adr): record slice 3c persistence + unknown-default decisions (ADR-0020) Pin the resolution reached in the grill: planning status persists as a per-UPRN write-through cache in the existing `property_details_spatial` table (not FE-property columns), read back off the Property in Modelling; unknown UPRN defaults to unrestricted, matching legacy `empty_spatial_df` (superseding the earlier "conservative stance" note). Co-Authored-By: Claude Opus 4.8 --- .../0020-conservation-status-as-property-attributes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/adr/0020-conservation-status-as-property-attributes.md b/docs/adr/0020-conservation-status-as-property-attributes.md index ba487026..fd966170 100644 --- a/docs/adr/0020-conservation-status-as-property-attributes.md +++ b/docs/adr/0020-conservation-status-as-property-attributes.md @@ -12,6 +12,14 @@ Wall Insulation Eligibility (ADR-0019) — and later Solar-PV and Windows genera The conservation/listed/heritage flags are **co-located with longitude/latitude in the same S3 partition** the geospatial Repo already reads — so we **extend the existing `GeospatialS3Repository`** to surface those extra columns alongside the coordinates, rather than porting a separate spatial-join or reading the legacy `property_spatial` table. A further deep-dive into the exact S3 columns/shape precedes slice 3. +## Persistence (decided, slice 3c) + +The OS Open-UPRN reference set is **tens of millions of rows** — too large to host in Postgres — so it lives in S3 and is resolved on demand. The flags must nonetheless reach the front-end (the FE displays them on the `property_details_spatial` view so a user can see *why* a Property did or did not get a given measure), and the FE reads Postgres, not S3. So Ingestion follows a **write-through cache**: fetch the spatial reference row from S3, use the coordinates to drive the Solar fetch, and **persist the whole row (coordinates + three flags) into the existing `property_details_spatial` table, keyed by UPRN** (one shared row per UPRN, not per Property — `uprn` is unique; cf. legacy `bulk_upsert_property_spatial`'s `on_conflict_do_update`). Modelling reads the flags back **off the Property** (`PropertyPostgresRepository` hydrates `Property.planning_restrictions` from that table by UPRN) — the stage boundary stays repo-mediated (ADR-0011); Modelling never touches S3. + +This is the resolution of the earlier "persist onto the Property" wording: the flags are a Property *attribute* in the domain sense (an enrichment hydrated into the aggregate, exactly like the EPC is hydrated from `epc_property`), persisted in a **backend-written reference table**, not as columns on the FE-owned `property` row. + +**Unknown default — resolved to *unrestricted* (allow EWI).** When S3 has no row for a UPRN (or the cache has none yet), the Property hydrates to `PlanningRestrictions()` — all flags `False`. This matches what legacy actually does (`OpenUprnClient.empty_spatial_df` sets all three flags `False`; the nearest-UPRN proxy keeps the flags and only nulls coordinates), so the Consequences note below — which read legacy as conservatively *blocking* on missing data — was mistaken about legacy and is superseded: we do **not** suppress a valid measure on absent evidence. UPRN is now a **required** identifier (it stitches the datasets together), so "Property with no UPRN" is an edge handled by the same unrestricted fallback rather than a designed-for case. + ## Consequences - Generators that need planning status take it as an input or read it off the Property; it never lands on `EpcPropertyData`.