JevDSL.Cache behaviour (jev_dsl v0.1.0)

Copy Markdown View Source

Deterministic identity hashing, confidence-aware cacheability rules, and pluggable two-tier (L1 fast key-value + L2 durable store) caching behaviour for JevDSL.

Cache Key Invariance & Auto-Invalidation

Every cache key ("jev_<sha256>") is derived deterministically from:

  1. The recursively canonicalized input state map (canonicalize/1).
  2. The compile-time %SystemOneSDK.Prepared{fingerprint: fp} SHA-256 digest of the contract's instructions, question types, and criteria.

Because prepared.fingerprint changes whenever a prompt or criterion is edited in defnoul, defchoice, defscore, or defjev, stale cached evaluations are invalidated automatically at compile time without manual cache flushing.

Telemetry Events

JevDSL.Cache emits the following :telemetry events:

  • [:jev_dsl, :cache, :hit] — Measurements: %{duration: native_time}. Metadata: %{cache_key: String.t(), adapter: module(), tier: :l1 | :l2, contract: atom() | String.t() | nil}.

  • [:jev_dsl, :cache, :miss] — Measurements: %{duration: native_time}. Metadata: %{cache_key: String.t(), adapter: module(), tier: :l1 | :l2, contract: atom() | String.t() | nil}.

  • [:jev_dsl, :cache, :write] — Measurements: %{duration: native_time}. Metadata: %{cache_key: String.t(), adapter: module(), tier: :l1 | :l2, contract: atom() | String.t() | nil}.

Implementing a Custom Cache Adapter

defmodule MyApp.JevCacheAdapter do
  @behaviour JevDSL.Cache

  @impl true
  def get(key, _opts) do
    case MyApp.Cache.get(key) do
      {:ok, value} when not is_nil(value) -> {:ok, value}
      _ -> :miss
    end
  end

  @impl true
  def put(key, value, opts) do
    ttl = Keyword.get(opts, :ttl, 604_800)
    MyApp.Cache.put(key, value, ttl: ttl)
  end
end

Summary

Functions

Reads a key from the given adapter module implementing JevDSL.Cache.

Batch-reads keys from adapter using get_many/2 if exported, falling back to adapter_get/3. Returns a map of %{cache_key => cached_value} for all hits.

Writes a key/value to the given adapter module if resolve_ttl/2 approves caching.

Batch-writes [batch_entry()] to an adapter via put_many/2 if exported, falling back to put/3.

Returns true if a SystemOneSDK response meets the minimum confidence threshold for long-lived caching (:min_confidence, default 0.65).

Recursively normalizes structs, maps, lists, and tuples into a canonical form with string keys for deterministic binary serialization.

Computes a deterministic <prefix>_<sha256> key for any map structure.

Builds the canonical identity map combining the evaluation state and contract fingerprint.

Computes a deterministic "jev_<sha256>" cache key from state and a %SystemOneSDK.Prepared{} contract (or fingerprint string / contract key with :contract_module).

Returns true if the evaluation result is a valid SystemOneResponse eligible for durable Tier-2 persistence.

Determines the appropriate TTL in seconds for an evaluation result (:ok tuple), or :skip if the result is an error that should not be cached.

Types

batch_entry()

@type batch_entry() :: {cache_key(), atom() | String.t(), map(), term()}

cache_key()

@type cache_key() :: String.t()

Callbacks

get(key, opts)

@callback get(key :: cache_key(), opts :: keyword()) ::
  {:ok, term()} | :miss | {:error, term()}

get_many(keys, opts)

(optional)
@callback get_many(keys :: [cache_key()], opts :: keyword()) :: %{
  optional(cache_key()) => term()
}

put(key, value, opts)

@callback put(key :: cache_key(), value :: term(), opts :: keyword()) ::
  :ok | {:error, term()}

put_many(entries, opts)

(optional)
@callback put_many(entries :: [batch_entry()], opts :: keyword()) ::
  :ok | {:error, term()}

Functions

adapter_get(adapter, key, opts)

@spec adapter_get(module() | nil, cache_key(), keyword()) :: {:ok, term()} | :miss

Reads a key from the given adapter module implementing JevDSL.Cache.

adapter_get_many(adapter, keys, opts)

@spec adapter_get_many(module() | nil, [cache_key()], keyword()) :: %{
  optional(cache_key()) => term()
}

Batch-reads keys from adapter using get_many/2 if exported, falling back to adapter_get/3. Returns a map of %{cache_key => cached_value} for all hits.

adapter_put(adapter, key, value, opts)

@spec adapter_put(module() | nil, cache_key(), term(), keyword()) :: :ok

Writes a key/value to the given adapter module if resolve_ttl/2 approves caching.

adapter_put_many(adapter, entries, opts)

@spec adapter_put_many(module() | nil, [batch_entry()], keyword()) :: :ok

Batch-writes [batch_entry()] to an adapter via put_many/2 if exported, falling back to put/3.

cacheable?(result, opts \\ [])

@spec cacheable?(term(), keyword()) :: boolean()

Returns true if a SystemOneSDK response meets the minimum confidence threshold for long-lived caching (:min_confidence, default 0.65).

canonicalize(struct)

@spec canonicalize(term()) :: term()

Recursively normalizes structs, maps, lists, and tuples into a canonical form with string keys for deterministic binary serialization.

Examples

iex> JevDSL.Cache.canonicalize(%{a: 1, nested: {%{b: 2}}})
%{"a" => 1, "nested" => {:__tuple__, [%{"b" => 2}]}}

hash_identity(prefix, identity_map)

@spec hash_identity(String.t(), map()) :: cache_key()

Computes a deterministic <prefix>_<sha256> key for any map structure.

identity(state, prepared, opts \\ [])

@spec identity(term(), SystemOneSDK.Prepared.t() | String.t() | term(), keyword()) ::
  map()

Builds the canonical identity map combining the evaluation state and contract fingerprint.

key(state, prepared, opts \\ [])

@spec key(term(), SystemOneSDK.Prepared.t() | String.t() | term(), keyword()) ::
  cache_key()

Computes a deterministic "jev_<sha256>" cache key from state and a %SystemOneSDK.Prepared{} contract (or fingerprint string / contract key with :contract_module).

Examples

iex> key1 = JevDSL.Cache.key(%{a: 1, b: [%{c: 2}]}, "fp_1")
iex> key2 = JevDSL.Cache.key(%{"a" => 1, "b" => [%{"c" => 2}]}, "fp_1")
iex> key1 == key2
true
iex> String.starts_with?(key1, "jev_")
true

persistable?(result, opts)

@spec persistable?(term(), keyword()) :: boolean()

Returns true if the evaluation result is a valid SystemOneResponse eligible for durable Tier-2 persistence.

resolve_ttl(result, opts \\ [])

@spec resolve_ttl(term(), keyword()) :: {:ok, pos_integer()} | :skip

Determines the appropriate TTL in seconds for an evaluation result (:ok tuple), or :skip if the result is an error that should not be cached.