Legacy property columns no longer resolve type or built form 🟩

ADR-0056 amended: override and EPC own type/form facts; a property with
neither is Unknown whatever the legacy columns say. Mirror columns removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-07 12:52:11 +00:00
parent ddd64a9297
commit 07190fc332
3 changed files with 19 additions and 27 deletions

View file

@ -52,12 +52,12 @@ class _ComponentResolution:
"""How one filterable component (property_type / built_form) resolves at
each ADR-0056 precedence level: which override row names it, how its RdSAP
numeric codes map (text labels pass through as-is), and which columns hold
it on the EPC and the legacy property row."""
it on the EPC. The legacy property.property_type / built_form columns are
NOT consulted (ADR-0056 amendment overrides own those facts)."""
override_component: str
codes: dict[str, str]
epc_columns: tuple[str, ...]
legacy_column: str
_PROPERTY_TYPE = _ComponentResolution(
@ -71,7 +71,6 @@ _PROPERTY_TYPE = _ComponentResolution(
},
# A cert without the property_type column falls back to its dwelling_type.
epc_columns=("property_type", "dwelling_type"),
legacy_column="property_type",
)
_BUILT_FORM = _ComponentResolution(
@ -85,7 +84,6 @@ _BUILT_FORM = _ComponentResolution(
"6": "Enclosed Mid-Terrace",
},
epc_columns=("built_form",),
legacy_column="built_form",
)
@ -94,12 +92,12 @@ def resolve_filtered_property_ids(
) -> list[FilteredProperty]:
"""Resolve *filters* against *portfolio_id* per ADR-0056: base set is the
portfolio's rows with ``marked_for_deletion = false``; property_type /
built_form resolve override lodged EPC predicted EPC legacy columns
"Unknown". Filters AND-combine; an absent key is unconstrained."""
built_form resolve override lodged EPC predicted EPC "Unknown".
Filters AND-combine; an absent key is unconstrained."""
# IS NOT TRUE rather than = false: the FE schema defaults the flag, but a
# NULL must never silently exclude a property.
base_sql = (
"SELECT id, postcode, property_type, built_form FROM property"
"SELECT id, postcode FROM property"
" WHERE portfolio_id = :portfolio_id"
" AND marked_for_deletion IS NOT TRUE"
)
@ -109,14 +107,7 @@ def resolve_filtered_property_ids(
params["postcodes"] = filters.postcodes
base_sql += " ORDER BY id"
candidates = [
_Candidate(
property_id=int(row[0]),
postcode=row[1],
legacy_values={
_PROPERTY_TYPE.legacy_column: row[2],
_BUILT_FORM.legacy_column: row[3],
},
)
_Candidate(property_id=int(row[0]), postcode=row[1])
for row in session.connection().execute(text(base_sql), params)
]
@ -142,7 +133,6 @@ def resolve_filtered_property_ids(
class _Candidate:
property_id: int
postcode: Optional[str]
legacy_values: dict[str, Optional[str]]
def _resolved_component_values(
@ -150,7 +140,7 @@ def _resolved_component_values(
) -> dict[int, str]:
"""Each candidate's effective value for *component* per the ADR-0056
precedence: override (building_part 0) lodged EPC predicted EPC
legacy column "Unknown"."""
"Unknown"."""
property_ids = [c.property_id for c in candidates]
override_rows = session.connection().execute(
@ -186,7 +176,6 @@ def _resolved_component_values(
by_override.get(c.property_id)
or by_epc_source["lodged"].get(c.property_id)
or by_epc_source["predicted"].get(c.property_id)
or c.legacy_values.get(component.legacy_column)
or _UNKNOWN
)
for c in candidates

View file

@ -40,9 +40,7 @@ lands in both codebases and amends this ADR.
built_form: 1=Detached, 2=Semi-Detached, 3=End-Terrace, 4=Mid-Terrace,
5=Enclosed End-Terrace, 6=Enclosed Mid-Terrace. Text passes through
as-is;
3. else the legacy `property.property_type` / `property.built_form`
columns;
4. else **"Unknown"** — a selectable filter value meaning exactly this
3. else **"Unknown"** — a selectable filter value meaning exactly this
bucket (no resolvable value at any level).
4. An absent filter key is unconstrained; present keys combine with **AND**.
@ -57,3 +55,11 @@ lands in both codebases and amends this ADR.
- Drift risk is accepted and mitigated by this document; a cross-repo
contract test (same fixture set, both implementations) is the natural
follow-on if drift ever bites.
## Amendment (2026-07-08): the legacy property columns are not consulted
The original rule fell back to `property.property_type` / `property.built_form`
before "Unknown". Those columns are legacy: the override layer owns
user-supplied type/form facts, so a property with no override and no EPC is
**"Unknown"** whatever the legacy columns say. The app's preview must apply
the same amendment.

View file

@ -57,10 +57,7 @@ class PropertyRow(SQLModel, table=True):
has_recommendations: Optional[bool] = Field(default=None)
updated_at: Optional[datetime] = Field(default=None)
# Columns the Modelling Run filter resolution reads (ADR-0056): the base-set
# exclusion flag and the legacy type/form columns (the last resort before
# "Unknown" in the resolution precedence). NOT NULL / defaulted in the FE
# schema; nullable mirrors are safe — prod enforces the real constraints.
# The Modelling Run filter resolution's base-set exclusion flag (ADR-0056).
# The legacy property_type / built_form columns are deliberately NOT
# mirrored: overrides and EPCs own those facts (ADR-0056 amendment).
marked_for_deletion: Optional[bool] = Field(default=None)
property_type: Optional[str] = Field(default=None)
built_form: Optional[str] = Field(default=None)