Class: Insika::FunnelStore
- Inherits:
-
Object
- Object
- Insika::FunnelStore
- Defined in:
- lib/insika/funnel_store.rb
Overview
the durable aggregates of the OUTCOME FUNNEL — per-day stage counts (the fold's cells), the fold cursor and the baseline snapshot. A dumb domain store: it holds no outcome_store and never folds (C4 owns the transformation), and it is recomputable by construction — the OutcomeStore stays the source of truth.
Key shapes (string keys, Store-contract JSON):
"funnel" "acme:store-support:2026-08-14" -> { "greeted" => 41, "paid" => 3 }
"funnel_cursor" "acme:store-support" -> { "at" => ISO8601 | nil, "ids" => [uuid, …] }
"funnel_baseline" "acme:store-support" -> { from/to/stages/primary/…/frozen_at }
The no-tenant case is the literal "platform" (the outcome KEY's rule), so
funnel and outcome keys share the same tenant segment and the purge prefix
scan is the same string. A record's tenant field is tenant.to_s — "" for
a single-tenant write — so every key-builder normalizes a blank tenant to
"platform" HERE (one place), or a single-tenant fold lands in an ""-prefixed
cell that purge("platform") never removes.
Constant Summary collapse
- SCOPE =
day cells
"funnel"- CURSOR_SCOPE =
per (tenant, agent)
"funnel_cursor"- BASELINE_SCOPE =
per (tenant, agent)
"funnel_baseline"
Instance Method Summary collapse
-
#add(tenant:, agent:, at:, counts:) ⇒ Object
Cumulative increment (D2):
countsis the fold's { stage => 1 } hash for the reached prefix stages — the STORE stays free of the declaration (D1), it only accumulates. -
#baseline(tenant:, agent:) ⇒ Object
-> Hash | nil — the current baseline record (D5), read back verbatim.
-
#cursor(tenant:, agent:) ⇒ Object
The fold cursor of the pair.
-
#day(tenant:, agent:, day:) ⇒ Object
-> { stage => count } | {} — one day's cell ("YYYY-MM-DD").
-
#days(tenant:, agent:, from: nil, to: nil) ⇒ Object
-> { "YYYY-MM-DD" => { stage => count } } sorted ascending, bounded by ISO-date strings from:/to: (inclusive).
-
#delete_days(tenant:, agent:) ⇒ Object
wipes ONE pair's day cells — the recompute repair path (the fold rebuilds them from the outcome store, so it must start from zero, never sum on top).
-
#delete_older_than(time) ⇒ Object
Age-based prune of DAY CELLS ONLY (retention, WS8 — outcomes and their fold die together).
-
#initialize(store:) ⇒ FunnelStore
constructor
A new instance of FunnelStore.
-
#pairs ⇒ Object
The pairs that have any day cell — the Studio's derived drill (D7).
-
#purge(tenant:) ⇒ Object
Purge one tenant: its day cells, cursors and baselines (DeleteTenantData, WS8 — the tenant is the FIRST key segment, so a prefix scan; the key IS the isolation).
-
#set_baseline(tenant:, agent:, record:) ⇒ Object
Overwrites (D5 — one current snapshot per pair, no history).
- #set_cursor(tenant:, agent:, at:, ids:) ⇒ Object
Constructor Details
#initialize(store:) ⇒ FunnelStore
Returns a new instance of FunnelStore.
28 29 30 |
# File 'lib/insika/funnel_store.rb', line 28 def initialize(store:) @store = store end |
Instance Method Details
#add(tenant:, agent:, at:, counts:) ⇒ Object
Cumulative increment (D2): counts is the fold's { stage => 1 } hash for
the reached prefix stages — the STORE stays free of the
declaration (D1), it only accumulates. Bumped in ONE transaction.
-> { stage => count } the NEW day counts (string keys, declared order).
36 37 38 39 40 41 42 43 44 45 |
# File 'lib/insika/funnel_store.rb', line 36 def add(tenant:, agent:, at:, counts:) id = pair_id(tenant, agent) day_key = day_segment(at) @store.transaction do cell = @store.get(SCOPE, "#{id}:#{day_key}") || {} counts.each { |stage, n| cell[stage.to_s] = cell[stage.to_s].to_i + n } @store.set(SCOPE, "#{id}:#{day_key}", cell) cell end end |
#baseline(tenant:, agent:) ⇒ Object
-> Hash | nil — the current baseline record (D5), read back verbatim.
75 76 77 |
# File 'lib/insika/funnel_store.rb', line 75 def baseline(tenant:, agent:) @store.get(BASELINE_SCOPE, pair_id(tenant, agent)) end |
#cursor(tenant:, agent:) ⇒ Object
The fold cursor of the pair. -> { "at" => String | nil, "ids" => [String] }
65 66 67 68 |
# File 'lib/insika/funnel_store.rb', line 65 def cursor(tenant:, agent:) record = @store.get(CURSOR_SCOPE, pair_id(tenant, agent)) { "at" => record && record["at"], "ids" => Array(record && record["ids"]) } end |
#day(tenant:, agent:, day:) ⇒ Object
-> { stage => count } | {} — one day's cell ("YYYY-MM-DD").
48 49 50 |
# File 'lib/insika/funnel_store.rb', line 48 def day(tenant:, agent:, day:) @store.get(SCOPE, "#{pair_id(tenant, agent)}:#{day}") || {} end |
#days(tenant:, agent:, from: nil, to: nil) ⇒ Object
-> { "YYYY-MM-DD" => { stage => count } } sorted ascending, bounded by ISO-date strings from:/to: (inclusive). Empty hash when none.
54 55 56 57 58 59 60 61 62 |
# File 'lib/insika/funnel_store.rb', line 54 def days(tenant:, agent:, from: nil, to: nil) prefix = "#{pair_id(tenant, agent)}:" @store.list(SCOPE).select { |k| k.start_with?(prefix) }.each_with_object({}) do |k, acc| day = k.delete_prefix(prefix) next unless within?(day, from, to) acc[day] = @store.get(SCOPE, k) end.sort.to_h end |
#delete_days(tenant:, agent:) ⇒ Object
wipes ONE pair's day cells — the recompute repair path (the
fold rebuilds them from the outcome store, so it must start from zero,
never sum on top). Same tenant normalization as every other key builder
(""/nil/"platform" all reach the "platform" segment). -> count removed.
117 118 119 120 121 122 |
# File 'lib/insika/funnel_store.rb', line 117 def delete_days(tenant:, agent:) prefix = "#{pair_id(tenant, agent)}:" keys = @store.list(SCOPE).select { |k| k.start_with?(prefix) } keys.each { |k| @store.delete(SCOPE, k) } keys.size end |
#delete_older_than(time) ⇒ Object
Age-based prune of DAY CELLS ONLY (retention, WS8 — outcomes and their fold die together). The day is the key's last segment ("YYYY-MM-DD", lexicographic). Cursors/baselines are tiny and live while their agent does. -> count removed.
100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/insika/funnel_store.rb', line 100 def delete_older_than(time) cutoff = time.utc.strftime("%Y-%m-%d") removed = 0 @store.list(SCOPE).each do |k| day = k.rpartition(":").last next unless day < cutoff @store.delete(SCOPE, k) removed += 1 end removed end |
#pairs ⇒ Object
The pairs that have any day cell — the Studio's derived drill (D7). -> [{ tenant: String | nil, agent: String }]
126 127 128 |
# File 'lib/insika/funnel_store.rb', line 126 def pairs @store.list(SCOPE).map { |k| pair_of(k) }.uniq end |
#purge(tenant:) ⇒ Object
Purge one tenant: its day cells, cursors and baselines (DeleteTenantData, WS8 — the tenant is the FIRST key segment, so a prefix scan; the key IS the isolation). -> count removed.
87 88 89 90 91 92 93 94 |
# File 'lib/insika/funnel_store.rb', line 87 def purge(tenant:) prefix = "#{tenant_id(tenant)}:" [SCOPE, CURSOR_SCOPE, BASELINE_SCOPE].sum do |scope| keys = @store.list(scope).select { |k| k.start_with?(prefix) } keys.each { |k| @store.delete(scope, k) } keys.size end end |
#set_baseline(tenant:, agent:, record:) ⇒ Object
Overwrites (D5 — one current snapshot per pair, no history).
80 81 82 |
# File 'lib/insika/funnel_store.rb', line 80 def set_baseline(tenant:, agent:, record:) @store.set(BASELINE_SCOPE, pair_id(tenant, agent), record) end |
#set_cursor(tenant:, agent:, at:, ids:) ⇒ Object
70 71 72 |
# File 'lib/insika/funnel_store.rb', line 70 def set_cursor(tenant:, agent:, at:, ids:) @store.set(CURSOR_SCOPE, pair_id(tenant, agent), { "at" => at, "ids" => Array(ids) }) end |