# frozen_string_literal: true # Helpdesk::Ledger — the PURE, network-free deterministic reducer that maintains a caller's yes/no # "resolutions ledger". It never flips a past decision in place: a reversal appends a new entry, retains # the old one (superseded_by), and flags a contradiction for a human. Matching is by canonical entry id # (supplied by the summariser) with an embedding cosine backstop; vectors are passed IN (never embedded # here) so this stays offline-testable and safe to call under the store lock. grep anchor: resolutions-ledger. module Helpdesk module Ledger module_function MATCH_THRESHOLD = 0.86 # cosine >= this = "same question" (calibrate offline against Czech paraphrases) # --- vector packing: base64 Float32, single line even under JSON.pretty_generate --- def pack_vec(floats) = [Array(floats).pack("g*")].pack("m0") def unpack_vec(str) return nil if str.to_s.empty? str.unpack1("m0").unpack("g*") rescue StandardError nil end def cosine(a, b) return 0.0 unless a.is_a?(Array) && b.is_a?(Array) && a.length == b.length && !a.empty? dot = na = nb = 0.0 i = 0 while i < a.length dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; i += 1 end d = Math.sqrt(na) * Math.sqrt(nb) d.zero? ? 0.0 : dot / d end VALID_DECISIONS = %w[yes no conditional].freeze def normalize_decision(x) = VALID_DECISIONS.include?(x.to_s) ? x.to_s : "conditional" # Apply one call's ledger_deltas to `ledger`. Pure: no store, no network. Idempotent per event_id. # Returns { ledger:, changed:, applied_event_ids: }. def apply(ledger:, deltas:, event_id:, started_at:, applied_event_ids:, entry_vec:, next_id:) applied = Array(applied_event_ids) return { ledger: ledger, changed: false, applied_event_ids: applied } if applied.include?(event_id) work = ledger.map(&:dup) # copy-on-write; never mutate caller's entry hashes by_id = work.each_with_object({}) { |e, h| h[e["id"]] = e } changed = false Array(deltas).each_with_index do |dlt, idx| decision = normalize_decision(dlt["decision"]) entry = resolve_entry(dlt, work, by_id, entry_vec) if entry.nil? e = { "id" => next_id.call, "question" => dlt["question"].to_s, "decision" => decision, "first_at" => started_at, "last_at" => started_at, "last_event_id" => event_id, "quote" => dlt["quote"].to_s, "vec_ref" => { "event_id" => event_id, "idx" => idx } } # embed-failed (nil vector) can never be cosine-matched -> flag for human review, never silently merge e["contradiction"] = true if dlt["question_vec"].nil? work << e; by_id[e["id"]] = e; changed = true elsif entry["decision"] == decision entry["last_at"] = started_at; entry["last_event_id"] = event_id; changed = true else # different decision = reversal. Never mutate in place. Append the new decision. neu = { "id" => next_id.call, "question" => entry["question"], "decision" => decision, "first_at" => started_at, "last_at" => started_at, "last_event_id" => event_id, "quote" => dlt["quote"].to_s, "vec_ref" => { "event_id" => event_id, "idx" => idx }, "contradiction" => true } entry["contradiction"] = true # only a strictly-LATER event supersedes the current decision (H1: out-of-order safe). entry["superseded_by"] = neu["id"] if later?(started_at, entry["last_at"]) work << neu; by_id[neu["id"]] = neu; changed = true end end { ledger: work, changed: changed, applied_event_ids: applied + [event_id] } end # canonical id -> existing (non-superseded) entry; else cosine backstop; else nil (=> new entry). def resolve_entry(dlt, work, by_id, entry_vec) id = dlt["entry_id"].to_s unless id.empty? e = by_id[id] return e if e && !e["superseded_by"] # unknown/foreign/superseded id -> fall through to backstop end qv = dlt["question_vec"] qv = Ledger.unpack_vec(qv) if qv.is_a?(String) return nil unless qv.is_a?(Array) && !qv.empty? && entry_vec best = nil; best_sim = 0.0 work.each do |e| next if e["superseded_by"] ev = entry_vec.call(e) s = Ledger.cosine(qv, ev) (best = e; best_sim = s) if s > best_sim end best_sim >= MATCH_THRESHOLD ? best : nil end # strictly-later comparison on ISO-8601 strings; nil sorts oldest so a missing ts never supersedes. def later?(a, b) return false if a.nil? return true if b.nil? a.to_s > b.to_s end end end