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>
5 KiB
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 5a–5e, all on feature/epc-prediction)
- 5a
Property.predicted_epcslot + a"predicted"source_path/effective_epcbranch — used only when there's no lodged EPC and no Site Notes (a real source always wins). - 5b
ComparablePropertiesRepository.candidates_for(postcode)+EpcComparablePropertiesRepositoryadapter (postcode search → per-cert fetch → batched UPRN→coords). Composes withEpcClientService+GeospatialS3Repository. - 5c EPC store
sourcediscriminator (lodged|predicted) so the two coexist per property;get_predicted_for_property/_for_properties;PropertyPostgresRepositoryhydratespredicted_epc. Needs a DB migration — seedocs/MIGRATION_NOTE_predicted_epc_source.md. - 5d
build_prediction_target(identity, coords, attributes)+ the eligibility gate (unknownproperty_type→ not predicted). Override attributes come through thePredictionTargetAttributesReaderport (the stub). - 5e
IngestionOrchestratorwiring: 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:
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:
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.