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.
This commit is contained in:
Daniel Roth 2026-07-23 15:19:28 +00:00
parent d5fee8902f
commit 23c7b1dcdf
2 changed files with 18 additions and 2 deletions

View file

@ -269,8 +269,10 @@ export const HEATING_SYSTEM_OPTIONS: DescriptorBucket[] = [
{ label: "Community", needles: ["community"] },
{ label: "Solid Fuel", needles: ["coal", "anthracite", "smokeless", "wood", "dual fuel", "biomass", "solid fuel"] },
// NB: bare "oil" is a substring of "b(oil)er" — match the fuel token only (", oil"
// as in "Boiler and radiators, oil") and the leading override "Oil room heater".
{ label: "Oil", needles: [", oil", "oil room heater"] },
// as in "Boiler and radiators, oil") and the leading override vocabulary
// ("Oil room heater …", "Oil boiler, regular/combi"). "oil boiler" cannot
// collide with "solid fuel boiler", and Solid Fuel is checked first regardless.
{ label: "Oil", needles: [", oil", "oil room heater", "oil boiler"] },
{ label: "LPG", needles: ["lpg"] },
{ label: "Gas Room Heater", needles: ["gas room heater", "room heaters, mains gas", "room heaters, gas"] },
{ label: "Gas Boiler", needles: ["mains gas", "gas boiler", "gas cpsu"] },

View file

@ -507,6 +507,20 @@ describe("classifyDescriptor — wall / roof / heating buckets", () => {
expect(heat("Boiler and radiators, dual fuel (mineral and wood)")).toBe("Solid Fuel");
expect(heat("Boiler and radiators, lpg")).toBe("LPG");
});
// The non-gas wet-boiler archetypes: their fuel leads the string rather than
// following a comma, so they are the case the ", oil" needle cannot catch.
// Asserted by bucket rather than just "not Unknown", because the failure mode
// that matters is a *wrong* bucket — reorder the needles and "Oil boiler,
// combi" silently becomes a Gas Boiler, which the coverage test would pass.
it("buckets the non-gas wet-boiler override archetypes by their fuel", () => {
expect(heat("Oil boiler, regular")).toBe("Oil");
expect(heat("Oil boiler, combi")).toBe("Oil");
expect(heat("Solid fuel boiler")).toBe("Solid Fuel");
// Guard the collision the "oil boiler" needle could have caused: Solid Fuel
// is checked first, and a gas boiler still has no "oil" fuel token.
expect(heat("Gas boiler, combi")).toBe("Gas Boiler");
});
});
/** Legacy-property helper: only year_built is set, cutoff is false. */