Class: Insika::MemoryAuditStore
- Inherits:
-
Object
- Object
- Insika::MemoryAuditStore
- 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
-
.digest(value) ⇒ Object
The digest the callers share: SHA-256 hexdigest of JSON.generate(value) (or value.to_s for non-JSON scalars).
Instance Method Summary collapse
-
#for_cell(cell, limit: 100) ⇒ Object
-> [Entry] most recent first.
-
#initialize(store:, clock: nil) ⇒ MemoryAuditStore
constructor
A new instance of MemoryAuditStore.
-
#record(cell:, action:, actor:, key: nil, tenant: nil, customer: nil, old_hash: nil, new_hash: nil, note: nil) ⇒ Object
action: "put" | "forget" | "purge".
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" => , "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 |