Commit graph

7 commits

Author SHA1 Message Date
Daniel Roth
23c7b1dcdf fix(filters): bucket the oil boiler heating archetypes as Oil, not Unknown
#455 added three main_heating_system values — "Oil boiler, regular", "Oil
boiler, combi" and "Solid fuel boiler" — without a matching needle. The two
oil ones fell through every bucket to Unknown, which the vocabulary-coverage
test caught: an overridden component that displays but cannot be filtered
(ADR-0012).

The gap is specific to these archetypes. Every other oil value carries its
fuel *after* a comma ("Boiler and radiators, oil"), which the ", oil" needle
matches; these lead with it. Bare "oil" is unusable as a needle because it is
a substring of "b(oil)er", so the fix is the narrower "oil boiler" — which
cannot collide with "solid fuel boiler", and Solid Fuel is checked first
regardless. "Solid fuel boiler" already bucketed correctly.

One edit fixes both halves: the portfolio query's SQL bucketing reads the
same DESCRIPTOR_FILTER_CONFIG rather than restating the needles, so the
classifier and the SQL cannot drift here.

Also asserts these three by *bucket* rather than relying on the coverage
test's "not Unknown" — the failure that matters is a wrong bucket, and a
reordered needle list would silently make "Oil boiler, combi" a Gas Boiler
while the coverage test kept passing.
2026-07-23 15:19:28 +00:00
Khalim Conn-Kowlessar
3b04de622b feat(portfolio): Main Fuel EPC fallback + filterable Wall/Roof/Heating
Main Fuel no longer reads "Unknown" for new-approach properties. The new EPC
graph stores the main fuel as a JSONB scalar on
epc_main_heating_detail.main_fuel_type — a numeric RdSAP main_fuel code (~95%)
or an EPR text string (~5%). mainfuelSql now reads it (lodged over predicted)
instead of NULL; resolvedMainFuelSql keeps the override → EPC → 'Unknown'
precedence. The RdSAP code table (Model epc_codes.csv, cross-checked against
prod) + the EPR strings fold into MAINFUEL_OPTIONS, so the column label and the
filter work unchanged. resolveMainFuel gains the EPC branch as its pure twin;
coverage tests assert every code + string + override value is folded.
Verified: portfolio 824 now resolves code 26 → "Mains Gas".

Wall Type / Roof Type / Heating System become filterable. Their resolved values
are free-text (override vocab / epc_energy_element.description / legacy column)
with high cardinality, so exact dbValues can't scale — each coarse bucket is a
set of case-insensitive needles matched by prefix (wall/roof) or substring
(heating). classifyDescriptor is the pure twin of the SQL bucketing in
buildConditionSql; the "Unknown" bucket matches the complement of every known
needle. Buckets validated against the full prod vocabulary (heating Unknown =
28/640k rows). Wired through FilterField, FIELD_OPTIONS, buildConditionSql,
EPC_JOIN_FILTER_FIELDS and the count-query override joins; columns stay
free-text display. Coverage tests assert every Wall/Roof/MainHeatingSystem
override value buckets to a real, non-Unknown label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:30:50 +00:00
Khalim Conn-Kowlessar
4f40c0b13d feat(portfolio): smaller list fetch, larger export, wall/roof/heating columns
Three property-list improvements:

1. Fetch 250 rows initially, not 1000 (DEFAULT_LIMIT/DISPLAY_LIMIT). The table
   paginates at 7/page and isn't bulk-scrolled, so the join-heavy query stays
   fast; load-more still pulls the rest on demand.

2. Export now fetches its own batch (up to EXPORT_LIMIT) for the current filters
   instead of dumping the on-screen rows — so the small display fetch doesn't cap
   it — and the cap is raised 1000 -> 6000. Export button shows a loading state.

3. Add Wall Type / Roof Type / Heating System optional columns, resolved through
   the same override precedence as the others (override -> EPC epc_energy_element
   description, lodged over predicted -> legacy property_details_epc -> 'Unknown').
   Per-part components (wall/roof) read the main building part. New generic
   resolveOverrideDescriptor (+5 tests) with SQL twins in epcSources. Columns are
   display-only (no filter — the wall/roof override vocab is too large for chips)
   and included in the CSV export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:36:26 +00:00
Khalim Conn-Kowlessar
fc2f7d5b30 feat(portfolio): resolve Main Fuel column + filter via override precedence
Slice 4. The new EPC graph exposes no main-fuel string (documented gap), so for
new-approach properties the landlord override is the only source. resolveMainFuel
returns the raw value (override → legacy EPC → 'Unknown'); the cell's
resolveEnumLabel folds it to a chip. MAINFUEL_OPTIONS.dbValues extended with the
override vocabulary so overridden fuels are both displayable and filterable —
guarded by a coverage test over MainFuelValues. resolvedMainFuelSql mirrors it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:51:40 +00:00
Khalim Conn-Kowlessar
558e20f2fe feat(portfolio): band-native Construction Age column + filter (was Year Built)
Slice 3. Construction age is modelled as an RdSAP band, not a year (ADR-0012) —
the only representation that faithfully carries an 'Unknown' override. resolve-
ConstructionAge owns override(letter)→label / EPC band(letter)→label / legacy
year→band bucketing / 'Unknown', with 10 unit tests incl. band boundaries.
resolvedConstructionAgeSql mirrors it (emits the band label). YEAR_BUILT_OPTIONS
becomes the band labels + Unknown, guarded against label/en-dash drift by a new
epcSources.test. Column + filter + CSV header renamed to 'Construction Age'
(internal 'yearBuilt' key retained to avoid a wide rename).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:35:14 +00:00
Khalim Conn-Kowlessar
e2ab52e4f9 feat(portfolio): resolve Built Form column + filter via override precedence
Slice 2. Same shape as slice 1: resolveBuiltForm + builtFormOverrideJoin /
resolvedBuiltFormSql, wired into getProperties SELECT, the filter colMap, and the
count query's join set (builtForm added to EPC_JOIN_FILTER_FIELDS). BUILT_FORM_OPTIONS
gains Unknown (Not Recorded now matches only an explicit override of that literal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:26:07 +00:00
Khalim Conn-Kowlessar
3bf816130c feat(portfolio): resolve Property Type column + filter via override precedence
Slice 1 of the portfolio-list descriptor fix. Property Type was read from the
raw property row (p.property_type), which is NULL for new-approach properties
(updated_at >= 2026-06-01), so the column and filter broke for new portfolios.

- New pure resolver resolvePropertyType (descriptorResolution.ts, mirrors the
  provenance.ts split) owning override → EPC-derived (lodged over predicted,
  RdSAP codes mapped, legacy fallback) → 'Unknown', with 9 unit tests.
- SQL twin resolvedPropertyTypeSql + propertyTypeOverrideJoin in epcSources.ts.
- Wired into getProperties SELECT, the filter colMap, and the count query's
  conditional join set (propertyType added to EPC_JOIN_FILTER_FIELDS).
- PROPERTY_TYPE_OPTIONS gains Park home + Unknown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:21:22 +00:00