Skip to content

Recommendation matching engine

src/lib/server/recommend.ts is the core matcher of the Recommendation & Right-Sizing capability (#4). Given a buyer's selection answers, it evaluates the admin-authored rules (#59) and returns a deterministically ranked set of candidate systems, each carrying the rule(s) that recommended it and their rationale. The cost-tier right-sizing layer (#61) then steers this toward the most cost-appropriate option; the guided flow / API (#62) exposes it.

The engine is pure and driver-independent (like value typing): it takes already-read rules plus small product-side accessors, so it is unit-tested without a live D1 (recommend.spec.ts).

Inputs

ts
matchRecommendations(context, rules, opts) → { candidates: Candidate[] }
InputShapeSource
context{ optionIds: string[] }The buyer's selected factor options (guided flow). Order does not matter.
rulesRule[]getRules(db) — already enabled and non-retired.
opts.activeSystemIdsSet<string>Non-retired system ids (from listSystems). Retired systems are never recommended.
opts.attr(systemId, attributeId) => TypedValue | undefinedA system's typed attribute value, for attribute-threshold conditions.

Because getRules filters out disabled and retired rules, a rule that reaches the engine is fire-eligible by construction — enabling or disabling a rule (in #22) changes the recommendation with no code change.

How a rule contributes

A rule contributes its outcome systems when its conditions hold. Conditions come in two kinds (see the rule representation); all must hold:

  • option — tests the selection context: the buyer selected that factor option. A rule with no option conditions applies to every context (a global default rule).
  • attribute — tests a candidate system's typed value against operator threshold (e.g. stc >= 50). It validates that a recommended system genuinely meets the requirement. A system that lacks the value (untested / N/A / none / empty) fails the condition and is dropped.

So option conditions decide whether a rule engages this buyer, and attribute conditions decide which of the rule's outcome systems actually qualify.

Attribute comparison

evaluateAttributeCondition(value, operator, threshold):

  • Numeric (>=, <=, >, <) — derive a number from the typed value (numericOf). A range uses the bound the operator cares about: the max for lower-bound tests (>=/>, "can it reach at least X?") and the min for upper-bound tests. Booleans → 1/0; cost tiers → an ordinal (Budget 0 … Ultra Premium 3); dimensions/text → their first number.
  • includes — substring/label membership against the value's text or enum labels (case-insensitive).
  • = / != — numeric when both sides are numbers, else a normalized string compare.

Output & ranking

Each candidate:

ts
type Candidate = {
	systemId: string;
	role: 'preferred' | 'alternative'; // strongest role when several rules surface it
	budgetTier: string | null; // from the rule outcome (systems.budget_tier)
	matchedRuleIds: string[]; // best-priority first
	rationales: string[]; // aligned to matchedRuleIds
};

A system surfaced by multiple rules is merged once: the strongest role wins (preferred over alternative), and the rule ids / rationales are unioned (ordered by rule priority, then id).

Candidates are ordered deterministically by:

  1. rolepreferred before alternative;
  2. best matched-rule priority (lower first);
  3. best outcome rank within that rule (lower first);
  4. systemId ascending (final tie-break).

This ordering is total (the systemId tie-break guarantees no ambiguity) and is asserted directly in recommend.spec.ts.

Edge cases

  • No match — a selection that engages no rule (or whose rules yield no qualifying system) returns { candidates: [] }; callers render an empty state, never an error.
  • Retired system in an outcome — silently skipped (not in activeSystemIds), so a retired system never leaks into a recommendation even if a stale rule still lists it.
  • Missing attribute value — a thresholded system with no cell for that attribute fails the condition and is dropped, rather than being assumed to pass.
  • Global rule — a rule with only attribute conditions engages every context; use it for a default recommendation, gate it with an option condition otherwise.

Worked example

Rule high-stc-prefer-acoustical: condition option: acoustic-priority-high andattribute: acoustic-rating-stc-oitc >= 50; outcome preferred nw-acoustical-645, alternatives nw-aluminum-640 / sl45. A buyer who selected acoustic priority: high gets those three systems (each verified at STC ≥ 50), NW Acoustical 645 ranked first as the preferred; the alternatives carry their budget_tier so #61 can surface the cheapest one that still clears the bar. A buyer who did not select high acoustic priority gets no acoustic recommendation from this rule.