Class: Insika::MemoryAuditStore

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

Overview

append-only, per-cell audit of MEMORY mutations. Every operator mutation of a cell appends a line (who, when, what, old->new digests). The entry holds DIGESTS of values, never the values — the forget line records that a deletion happened without the deleted content (the digest is not invertible, and keys are fact names — provenance, not payload). Capped per cell (no retention hook, no delete path: the audit outlives what it describes). record rescues EVERYTHING — a failed audit write never fails the mutation it describes.

The capped-list RMW caveat (context_trace_store.rb's discipline): one cell key, written on the command's fiber; a cross-process race loses the loser's append — a trace-level loss, not a correctness one.

Defined Under Namespace

Classes: Entry

Constant Summary collapse

SCOPE =

store key = the memory cell scope

"memory_audit"
MAX_PER_CELL =

oldest dropped; the cap bounds growth

200

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(store:, clock: nil) ⇒ MemoryAuditStore

Returns a new instance of MemoryAuditStore.



27
28
29
30
# File 'lib/insika/memory_audit_store.rb', line 27

def initialize(store:, clock: nil)
  @store = store
  @clock = clock # -> Time, injectable for specs
end

Class Method Details

.digest(value) ⇒ Object

The digest the callers share: SHA-256 hexdigest of JSON.generate(value) (or value.to_s for non-JSON scalars). -> String



65
66
67
68
69
70
71
# File 'lib/insika/memory_audit_store.rb', line 65

def self.digest(value)
  payload = case value
            when Hash, Array then JSON.generate(value)
            else value.to_s
            end
  Digest::SHA256.hexdigest(payload)
end

Instance Method Details

#for_cell(cell, limit: 100) ⇒ Object

-> [Entry] most recent first. [] if none. A broken backend degrades to [] — the audit is read to RENDER, never to gate.



57
58
59
60
61
# File 'lib/insika/memory_audit_store.rb', line 57

def for_cell(cell, limit: 100)
  Array(@store.get(SCOPE, cell.to_s)).reverse.first(limit).map { |e| to_entry(e) }
rescue StandardError
  []
end

#record(cell:, action:, actor:, key: nil, tenant: nil, customer: nil, old_hash: nil, new_hash: nil, note: nil) ⇒ Object

action: "put" | "forget" | "purge". key = the fact name (provenance, not payload). Appends + caps. Rescues EVERYTHING -> Entry | nil (the audit never breaks a command).



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/insika/memory_audit_store.rb', line 35

def record(cell:, action:, actor:, key: nil, tenant: nil, customer: nil,
           old_hash: nil, new_hash: nil, note: nil)
  entry = {
    "at" => timestamp,
    "action" => action.to_s,
    "actor" => actor.to_s,
    "key" => Coercion.presence(key),
    "tenant" => Coercion.presence(tenant),
    "customer" => Coercion.presence(customer),
    "old_hash" => Coercion.presence(old_hash),
    "new_hash" => Coercion.presence(new_hash),
    "note" => Coercion.presence(note)
  }
  list = (@store.get(SCOPE, cell.to_s) || []) + [entry]
  @store.set(SCOPE, cell.to_s, list.last(MAX_PER_CELL))
  to_entry(entry)
rescue StandardError
  nil
end