Model/docs/HANDOVER_EPC_PREDICTION_WIRING.md
Khalim Conn-Kowlessar a43c03ed94 feat(epc-prediction): thread prediction injection points through the composition root
build_first_run_pipeline now constructs epc_prediction=EpcPrediction() and accepts
comparables_repo + prediction_attributes_reader as optional params, threading them
into IngestionOrchestrator (ADR-0031). The on-switch is now just supplying those
two arguments — no orchestrator/handler edits — once they exist: the cohort repo
(its EPC client is the source client pending #1136) and the property_overrides
attributes reader (built separately). Both default None, so the feature stays OFF
and ingestion is unchanged until they're passed.

The epc_property.source migration is live, so the predicted-EPC persistence slot
(slice-5c) now works against the real DB. Handover updated to reflect the simpler
composition-root step.

pyright strict clean; handler + pipeline + ingestion-prediction tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:53:54 +00:00

99 lines
5 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.

# EPC Prediction — production wiring handover (for Jun-te)
The EPC Prediction **gap-fill** is wired end-to-end behind seams, with one real
dependency stubbed: reading an EPC-less Property's resolved Landlord Overrides.
This note is what's needed to finish it once your `property_overrides` read path
lands. Design is **ADR-0031**; terms in **CONTEXT.md** (EPC Prediction, Effective
EPC, EPC Anomaly Flag).
## What's already built (slices 5a5e, all on `feature/epc-prediction`)
- **5a** `Property.predicted_epc` slot + a `"predicted"` `source_path` /
`effective_epc` branch — used only when there's no lodged EPC and no Site Notes
(a real source always wins).
- **5b** `ComparablePropertiesRepository.candidates_for(postcode)` +
`EpcComparablePropertiesRepository` adapter (postcode search → per-cert fetch →
batched UPRN→coords). Composes with `EpcClientService` + `GeospatialS3Repository`.
- **5c** EPC store `source` discriminator (`lodged` | `predicted`) so the two
coexist per property; `get_predicted_for_property` / `_for_properties`;
`PropertyPostgresRepository` hydrates `predicted_epc`. **Needs a DB migration —
see `docs/MIGRATION_NOTE_predicted_epc_source.md`.**
- **5d** `build_prediction_target(identity, coords, attributes)` + the eligibility
**gate** (unknown `property_type` → not predicted). Override attributes come
through the `PredictionTargetAttributesReader` port (the stub).
- **5e** `IngestionOrchestrator` wiring: when the three prediction collaborators
are injected, an EPC-less Property is predicted from its cohort and persisted to
the predicted slot. The collaborators are **optional** — unwired, ingestion is
unchanged.
## Your part — three things
### 1. Implement `PredictionTargetAttributesReader` (the stub)
`repositories/property/prediction_target_attributes_reader.py` defines the port:
`attributes_for(property_id) -> PredictionTargetAttributes` (property_type,
built_form, wall_construction). Build the adapter as a read over the
`property_overrides` fact layer (the finaliser writes it via
`PropertyOverrideRepository.upsert_all`; you're adding the read side).
**Code-space gotcha.** `select_comparables` filters
`comparable.epc.property_type == target.property_type`, and the cohort EPCs carry
gov **API codes** (e.g. `"0"`/`"2"`). Landlord Overrides resolve to enum *value*
strings (e.g. `"House"`). Your adapter must map override value → the API-code
space, or `property_type` will never match and every cohort comes back empty.
Same for `built_form`. (`domain/epc/property_type.py`, `built_form_type.py` are
the enums; `datatypes/epc/domain/epc_codes.csv` has the code table.)
`property_type` unresolved → return `PredictionTargetAttributes(property_type=None)`
so the gate skips the Property.
### 2. Run the Drizzle migration
`epc_property.source` column — full spec in
`docs/MIGRATION_NOTE_predicted_epc_source.md` (column + default `'lodged'` +
relax any `property_id` uniqueness to `(property_id, source)`).
### 3. Pass the two collaborators at the composition root
This is now wired: `build_first_run_pipeline` (in `applications/ara_first_run/handler.py`)
already constructs `epc_prediction=EpcPrediction()` and accepts the other two as
optional params that it threads into the `IngestionOrchestrator`. So the on-switch
is just supplying them once they exist:
```python
build_first_run_pipeline(
...,
comparables_repo=EpcComparablePropertiesRepository(epc_client, geospatial_repo),
prediction_attributes_reader=<your property_overrides adapter>, # task #1
)
```
`epc_client` is the same EPC source client behind `epc_fetcher` (the concrete
`EpcClientService` exposes `search_by_postcode` + `get_by_certificate_number`),
so build it alongside the other source clients in `_source_clients_from_env`
(pending #1136). Until **both** are passed, ingestion ignores prediction — no
orchestrator or handler edits needed, just the two arguments.
## One open item — Validation Cohort exclusion
A predicted-source Property has **no real lodged record**, so it must not be
scored as if it did (CONTEXT: Validation Cohort; ADR-0031 dec-3). There is **no
Validation-Cohort code path today** to exclude it from — when one is built (or in
any QA that compares `calc(effective_epc)` vs lodged), exclude on the structural
signal:
```python
if prop.source_path == "predicted":
continue # predicted EPC — no ground truth to validate against
```
Note too: `PropertyBaselinePerformance.lodged` is derived from `effective_epc`
regardless of source (`property_baseline_orchestrator``lodged_performance`), so
for a predicted Property that "lodged" is synthesised, not real. Decide whether
baseline should null/flag it for predicted properties when this lands.
## Anomaly dual-use (later, not now)
Slice-5 is gap-fill only (`epc is None`). The slot model already supports
predicting for *every* Property to compare predicted vs lodged (**EPC Anomaly
Flags**) — see ADR-0031 dec-4. Reuses the same `ComparableProperties` repo + the
predicted slot.