diff --git a/.gitignore b/.gitignore index b26a69b4..2d9e9175 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,6 @@ next-env.d.ts backlog/** -docs/adr/** # Personal Claude Code settings (per-developer, not shared) .claude/settings.local.json diff --git a/docs/adr/0007-postcode-search-creates-properties.md b/docs/adr/0007-postcode-search-creates-properties.md new file mode 100644 index 00000000..ca158d4b --- /dev/null +++ b/docs/adr/0007-postcode-search-creates-properties.md @@ -0,0 +1,60 @@ +# 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); +0004–0006 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.