Model/docs/MIGRATION_NOTE_predicted_epc_source.md
Khalim Conn-Kowlessar fd43cf2d23 feat(epc-prediction): slice-5c predicted-EPC persistence slot
Add a `source` discriminator (lodged | predicted) to the EPC store so a Property
holds a lodged EPC and a predicted one (EPC Prediction gap-fill) at once
(ADR-0031). EpcRepository.save gains source="lodged"; idempotent delete is now
per-source (a predicted save no longer wipes lodged, and vice versa);
get_for_property/get_for_properties filter lodged; new get_predicted_for_property
/ get_predicted_for_properties read predicted. PropertyPostgresRepository.get +
get_many hydrate Property.predicted_epc, so the predicted picture reaches the
modelling read (both load via get_many). FakeEpcRepo mirrors the dual slot.

EpcPropertyModel gains `source` (default "lodged"); the test DB builds from the
SQLModel mirror so this is exercised without the prod migration. The matching
Drizzle change (column + per-(property_id,source) uniqueness) is the team's to
action before merge — docs/MIGRATION_NOTE_predicted_epc_source.md.

3 store tests (coexist, idempotent predicted re-save leaves lodged, lodged-only
has no predicted) + property-repo wiring; 85 pass across affected suites; new
code pyright-clean (2 pre-existing wwhrs errors in epc_property_table untouched).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 03:50:19 +00:00

2.4 KiB

Migration note — epc_property.source (predicted-EPC slot)

For the team to action before merging the EPC Prediction production-wiring branch. The model-side code is done and tested against the SQLModel-built test DB; the production Drizzle schema needs a matching column that this repo does not own.

What changed in code (this branch)

Per ADR-0031, a Property can now hold a lodged EPC and a predicted EPC (EPC Prediction gap-fill) at the same time. The two are distinguished by a new source discriminator on the epc_property row:

  • infrastructure/postgres/epc_property_table.pyEpcPropertyModel gains source: str = Field(default="lodged").
  • repositories/epc/epc_postgres_repository.pysave(..., source="lodged") writes it; _delete_for_property is now per-source (idempotency no longer wipes the other slot); get_for_property / get_for_properties filter source = 'lodged'; new get_predicted_for_property / get_predicted_for_properties read source = 'predicted'.

The test database is built from the SQLModel mirrors via create_all, so tests already exercise the column. Production is not — hence this note.

Required Drizzle migration

On the epc_property table:

  1. Add column sourcetext (or your enum), NOT NULL, default 'lodged'. The default backfills every existing row as a real EPC, which is correct (all current rows are lodged).
  2. Relax any single-row-per-property uniqueness. If a unique constraint / index exists on epc_property(property_id), it must become (property_id, source) — a property may now have one lodged row and one predicted row. (Verify whether such a constraint exists; the SQLModel mirror has none, but the production schema may.)
  3. Recommended index (property_id, source) — every predicted/lodged read filters on both columns.

Allowed values

source ∈ {'lodged', 'predicted'} (see EpcSource in repositories/epc/epc_repository.py). No other values are written.

Why

ADR-0031: predicted EPCs are stored in their own slot rather than overwriting the lodged epc, so (a) provenance is structural — the Validation Cohort excludes predicted-sourced Properties and the UI flags them — and (b) lodged + predicted coexist, which the planned EPC Anomaly Flags feature needs (compare a Property's lodged EPC against its predicted one).