Class: Insika::Harvest::ConversionGate

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/harvest/conversion_gate.rb

Overview

C8 — the second gate: the store's target METRIC RATE over the criterion's window, compared to the frozen baseline's rate. Outcome is EVIDENCE — the only verdict this object can produce is "the store is measurably worse than the accepted state" and "there is nothing to compare against".

The ruler is a RATE (metric ÷ first stage), never a total: the frozen baseline covers a ≥ 28-day span while the window is 72 h — comparing the raw counts would flag a store that tripled its traffic as "worse" (fewer absolute sales in 3 days than in 28). Both sides fold the same denominator, so the comparison measures the per-unit health, which is what the audit can actually read back.

Refuse-with-a-named-reason, never pass, on missing data (the P18 lesson applied to the second ruler): no frozen baseline, no criterion, no funnel store, a fold that has not converged, a frozen span shorter than the criterion's min_span, a baseline whose rate cannot be computed.

Defined Under Namespace

Classes: Result

Instance Method Summary collapse

Constructor Details

#initialize(funnel_store:, criterion:) ⇒ ConversionGate

funnel_store: the FunnelStore (nil = the gate REFUSES — a store without a ruler cannot promote on outcome, D6). criterion: the boot-loaded Harvest::Criterion (nil = refuse).



40
41
42
43
# File 'lib/insika/harvest/conversion_gate.rb', line 40

def initialize(funnel_store:, criterion:)
  @funnel_store = funnel_store
  @criterion = criterion
end

Instance Method Details

#call(tenant:, agent:) ⇒ Object

-> Result. NEVER passes on missing data (D6).



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/insika/harvest/conversion_gate.rb', line 46

def call(tenant:, agent:)
  return refuse(:no_criterion) if @criterion.nil?
  return refuse(:no_funnel) if @funnel_store.nil?

  baseline = @funnel_store.baseline(tenant: tenant, agent: agent)
  return refuse(:no_frozen_baseline) if baseline.nil?

  # The criterion's metric is a stage NAME from the   declaration
  # (D5). The seed token "primary" resolves to the frozen snapshot's
  # primary stage — the declaration's own vocabulary, never a gem
  # constant .
  metric = resolve_metric(@criterion.rule.metric, baseline)
  return refuse(:metric_mismatch) if metric != baseline["primary"].to_s

  # min_span is LIVE (D5): the frozen baseline must cover the span the
  # criterion pre-registered — a number nobody re-froze to a shorter
  # span is not a number the gate may compare against.
  return refuse(:baseline_span_short) if span_short?(baseline, @criterion.rule.min_span)

  baseline_rate = rate_of(baseline["stages"] || {}, metric)
  # A baseline whose conversion was nil when frozen (no first-stage
  # events) cannot be the accepted state's ruler.
  return refuse(:no_baseline_rate) if baseline_rate.nil?

  window_hours = @criterion.rule.window.to_s.delete_suffix("h").to_i
  window_stages = fold_window(tenant: tenant, agent: agent, hours: window_hours)
  return refuse(:no_fold) if window_stages.empty?

  current_rate = rate_of(window_stages, metric)
  # The window folded no comparable denominator (no first-stage events
  # over the window) — there is nothing to compare.
  return refuse(:no_fold) if current_rate.nil?

  threshold = @criterion.rule.threshold.to_f
  snapshot_ref = "funnel:#{tenant_id(tenant)}:#{agent}:#{baseline['frozen_at']}"

  if current_rate >= baseline_rate * (1 - threshold)
    Result.new(passed: true, reason: nil, metric: metric,
               window: @criterion.rule.window.to_s, current: current_rate,
               baseline: baseline_rate, threshold: threshold,
               snapshot_ref: snapshot_ref)
  else
    Result.new(passed: false, reason: :conversion_down,
               metric: metric, window: @criterion.rule.window.to_s,
               current: current_rate, baseline: baseline_rate,
               threshold: threshold, snapshot_ref: snapshot_ref)
  end
end