Skip to main content

Inheritance dedupe

Without the flag, getEffectiveRules returns every rule whose scope matches the offer (global, category, subcategory, offer). When two rules share a name (e.g., a global “min_age=18” and an offer-specific “min_age=21”), both flow through. When inheritanceEnabled is on (or ?explainCascade=true is supplied), the resolver collapses duplicates to the most-specific scope per name using the precedence:
The dedupe is implemented by resolveEffectiveRules in lib/qualification/inheritance.ts.

Configuration

Default off — every existing tenant is unchanged. With the flag off, getEffectiveRules runs in inheritanceMode: "all", which returns the full matching rule set exactly as before (no dedupe, no time filter).

Time-aware windows

Rules can declare a config.timeWindow shape. The resolver checks each rule’s window against the request’s asOf timestamp when the route is called with ?asOf=ISO8601. Rules whose window does not include asOf are filtered out.

Window shape

All fields optional; unset fields don’t constrain. Conventions:
  • daysOfWeek uses 0 = Monday … 6 = Sunday, so [0, 1, 2, 3, 4] is Mon–Fri.
  • startHour / endHour are "HH:MM" 24-hour. Overnight windows are supported — if startHour > endHour (e.g. "22:00""06:00") the window wraps past midnight.
  • from / to are inclusive "YYYY-MM-DD" date bounds.
  • timezone is an IANA name (default UTC).
  • blackoutDates are "YYYY-MM-DD" dates that are always excluded.

Honest fail-open on malformed windows

When a rule has config.timeWindow set but the value is not a plain object, the filter falls open (the rule is preserved). This matches the existing “rules must work even when storage shape drifts” contract elsewhere in the codebase. Schema validation upstream is the place to enforce strict shape.

explainCascade

?explainCascade=true implies inheritance dedupe and populates cascadeTrace in the response — a per-name list of every candidate that contributed to the dedupe:
Useful for compliance UIs that need to render “rule X applies because of category Y” with a full chain.

Behavioral guarantees

The resolver (getEffectiveRules in lib/effective-rules.ts) upholds these invariants:
  • Default options preserve every matching rule. With inheritanceMode defaulting to "all" and no asOf, the output is unchanged from the pre-inheritance behavior — every rule whose scope matches the offer is returned.
  • inheritanceMode: "deduped" collapses duplicates to the single most-specific scope per rule name (precedence offer > subcategory > category > global).
  • explainCascade: true populates cascadeTrace with the per-name list of every contributing candidate, and implies deduped.
  • asOf filters by timeWindow: rules whose window excludes the instant are dropped; rules with no timeWindow, or whose window includes the instant, are kept.
  • Malformed timeWindow falls open — the rule is preserved rather than dropped.