assessment-model/docs/adr/0007-postcode-search-creates-properties.md
Khalim Conn-Kowlessar b86efddfdf chore(adr): stop gitignoring docs/adr; add ADR-0007 for postcode-search property creation
The docs/adr/** ignore rule silently ate ADRs 0004-0006 that CONTEXT.md
still references. Remove it so decisions actually land in the repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:07:43 +00:00

60 lines
2.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 7. Postcode search creates Properties directly, with OS-derived overrides
Date: 2026-07-06
## Status
Accepted
Numbering note: 0003 is taken by the app-authored-scenarios ADR (PR #353);
00040006 are referenced by CONTEXT.md but were lost to a `docs/adr/**`
.gitignore rule (removed in this commit). 0007 avoids all collisions.
## Context
Properties previously came into existence only via the BulkUpload finaliser or
the backend pipeline. The "Postcode search" journey (CONTEXT.md) lets a user
search a postcode (postcodes.io for validation + geography, OS Places for
addresses) and add selected addresses to a Portfolio directly — before any
modelling exists.
## Decision
- **Next.js inserts `property` rows directly**: address, postcode, uprn,
per-property lat/lng, local_authority (postcodes.io `admin_district`),
constituency (postcodes.io `parliamentary_constituency`). Nothing inferred —
no type/age/tenure guesses on the row. Energy state is derived: no EPC-graph
rows ⇒ "awaiting modelling" (mirrors ADR on app-authored scenarios).
- Dedup rides the existing partial unique index `uq_property_portfolio_uprn`
via `ON CONFLICT DO NOTHING`; the submit reports added vs already-existed.
- **Both override layers are written**, mimicking the finaliser: a
VocabularyMapping upsert per distinct OS classification code and a
`property_overrides` snapshot per property (original description = the OS
code). New `override_source` enum value **`os_places`** — the deterministic
OS-code mapping is not the LLM "classifier" and provenance must stay
distinguishable.
- **Deterministic mapping** (fixed table, tested): RD02→House+Detached,
RD03→House+Semi-Detached, RD04→House (Mid/End unknowable — no built form),
RD06→Flat, RD07→House. Every other code maps nothing; `Unknown` is never
written (absence, not noise) — preserving the finaliser's invariant. The
backend's predict-EPC service consumes these for archetype matching.
- **Read-through cache** on the existing `postcode_search` jsonb table:
serve when fresher than 30 days, else refresh from OS Places and upsert.
## Alternatives considered
- Routing through the BulkUpload pipeline (synthesise a one-postcode "upload"):
full provenance for free, but drags an interactive journey through an async
multi-stage state machine built for files.
- Property row carries property_type directly: bypasses the override layers the
Model backend actually consumes, and creates a second source of truth.
## Consequences
- A third Property creator exists; anything assuming "property ⇒ came from a
BulkUpload or the backend" is now wrong.
- `property_overrides` gains a second writer with `os_places` provenance;
re-classification tooling must not clobber user rows (existing rule) nor
treat OS rows as user-authored.
- Postcode-search-created properties render blank/awaiting-modelling in the
portfolio table and reporting until a scenario is modelled against them.