Class: Insika::Commands::FreezeFunnelBaseline

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/commands/freeze_funnel_baseline.rb

Overview

the operator's FREEZE — turn the folded counts over a declared span into the baseline snapshot (A/B follow-up) and (promotion gate) read. One command, dispatched from the Studio and available to any operator-grade caller. Synchronous control command — no task, no actor, same discipline as RecordOutcome (WS7).

The ≥ 4-week rule ("no 1.0 target before a remeasure") is enforced HERE, not by convention: a frozen span shorter than 28 days is a ValidationError. frozen_at stamps when; the baseline OVERWRITES (D5) — the freeze event is the audit surface.

Constant Summary collapse

MIN_SPAN_DAYS =
28

Instance Method Summary collapse

Constructor Details

#initialize(funnel_store:, profiles:, event_stream:) ⇒ FreezeFunnelBaseline

Returns a new instance of FreezeFunnelBaseline.



21
22
23
24
25
# File 'lib/insika/commands/freeze_funnel_baseline.rb', line 21

def initialize(funnel_store:, profiles:, event_stream:)
  @funnel_store = funnel_store
  @profiles = profiles
  @event_stream = event_stream
end

Instance Method Details

#call(command) ⇒ Object

payload: { agent:, tenant:, from: "YYYY-MM-DD", to: "YYYY-MM-DD", operator: } agent REQUIRED (ValidationError); tenant from payload || meta. from/to optional — default to the pair's folded span (first/last day cell); explicit from/to are inclusive bounds. -> the baseline record.

Raises:



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/insika/commands/freeze_funnel_baseline.rb', line 32

def call(command)
  payload = command.payload
  agent = Coercion.presence(payload[:agent] || payload["agent"])
  raise ValidationError, "agent is required" if agent.nil?

  declaration = declaration_for(agent)
  tenant = payload[:tenant] || payload["tenant"] || command.meta[:tenant]

  from, to = bounds(payload, tenant, agent)
  validate_span!(from, to)

  stages = @funnel_store.days(tenant: tenant, agent: agent, from: from, to: to)
                        .each_with_object(Hash.new(0)) do |(_, counts), acc|
    counts.each { |stage, n| acc[stage] += n }
  end
  primary = declaration.primary
  primary_count = stages[primary].to_i
  first_count = stages[declaration.first_stage].to_i
  conversion = first_count.zero? ? nil : primary_count.to_f / first_count

  baseline = {
    "from" => from, "to" => to, "stages" => stages,
    "primary" => primary, "primary_count" => primary_count,
    "conversion" => conversion, "window" => declaration.attribution_window,
    "frozen_at" => Time.now.utc.iso8601
  }
  @funnel_store.set_baseline(tenant: tenant, agent: agent, record: baseline)

  @event_stream.emit(Insika::Event.new(
                       type: :funnel_baseline_frozen,
                       data: { agent: agent, from: from, to: to,
                               primary: primary, primary_count: primary_count,
                               conversion: conversion },
                       meta: { at: Time.now.utc.iso8601 }
                     ))
  baseline
end