# `JevDSL.Cache`
[🔗](https://github.com/boxxxie/jev_dsl/blob/v0.1.0/lib/jev_dsl/cache.ex#L1)

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

# `batch_entry`

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

# `cache_key`

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

# `get`

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

# `get_many`
*optional* 

```elixir
@callback get_many(keys :: [cache_key()], opts :: keyword()) :: %{
  optional(cache_key()) =&gt; term()
}
```

# `put`

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

# `put_many`
*optional* 

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

# `adapter_get`

```elixir
@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`

```elixir
@spec adapter_get_many(module() | nil, [cache_key()], keyword()) :: %{
  optional(cache_key()) =&gt; 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`

```elixir
@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`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

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

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

# `identity`

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

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

# `key`

```elixir
@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?`

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

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

# `resolve_ttl`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
