# `ALLM.Providers.Fake.Script`
[🔗](https://github.com/cykod/ALLM/blob/v0.3.0/lib/allm/providers/fake/script.ex#L1)

Script shape detection, validation, and interpretation for
`ALLM.Providers.Fake`. See spec §31.

Layer B — pure helper module, no runtime state.

Fake accepts two disjoint script shapes:

- **Spec §31 (user-facing)** — the vocabulary the spec itself samples with
  `{:text, "hi"}, {:finish, :stop}`. Tags: `:text`, `:tool_call`,
  `:tool_call_delta`, `:usage`, `:raw_chunk`, `:finish`, `:error` (2-tuple),
  `:delay`, `:sleep` (deprecated alias of `:delay`).
- **Phase 3 harness** — the vocabulary `ALLM.Test.AdapterConformance` and
  `ALLM.Test.StreamAdapterConformance` pass to `StubAdapter`. Tags:
  `:ok`, `:error` (3-tuple), `:text_delta`, `:preflight_error`,
  `:error_event`, `:stream_error`, and shared-semantics `:tool_call` /
  `:finish`.

This module exposes four entry points:

- `detect_shape/1` — classify a list of entries by leading tag.
- `validate!/1` — guard `adapter_opts` at the Fake boundary.
- `fold_to_response/1` — reduce a list of entries into an `%ALLM.Response{}`
  (non-streaming path).
- `interpret/1` — translate one entry into a list of `ALLM.Event` values
  (streaming path).

See spec §31 for the script entry grammar; §8 for the event union.

# `harness_adapter_entry`

```elixir
@type harness_adapter_entry() :: {:ok, map()} | {:error, atom(), keyword()}
```

A single Phase 3 harness-shape entry (non-streaming).

# `harness_stream_entry`

```elixir
@type harness_stream_entry() ::
  {:text_delta, String.t()}
  | {:finish, atom()}
  | {:preflight_error, atom(), keyword()}
  | {:error_event, atom(), keyword()}
  | {:stream_error, atom(), keyword()}
```

A single Phase 3 harness-shape entry (streaming).

# `shape`

```elixir
@type shape() :: :spec31 | :harness
```

Detected shape of a script — §31 (user-facing) or Phase 3 harness.

# `spec31_entry`

```elixir
@type spec31_entry() ::
  {:text, String.t()}
  | {:tool_call, keyword()}
  | {:tool_call_delta, keyword()}
  | {:usage, map()}
  | {:raw_chunk, term()}
  | {:finish, ALLM.Response.finish_reason()}
  | {:error, term()}
  | {:delay, non_neg_integer()}
  | {:sleep, non_neg_integer()}
```

A single spec §31 script entry (one call's worth of events).

# `detect_shape`

```elixir
@spec detect_shape([term()]) :: {shape(), [term()]}
```

Classify a list of script entries as either `:spec31` or `:harness` shape.

Inspects only the leading entry's tag. The `:error` tag is disambiguated by
tuple arity — 2-tuple → `:spec31`, 3-tuple → `:harness`. Shared-semantics
tags (`:finish`, `:tool_call`) default to `:spec31` (the interpreter handles
both shapes identically for those tags, so the classification is
inconsequential).

An empty list returns `{:spec31, []}` (the default).

Raises `ArgumentError` if the leading tag is unknown, with a message that
mentions both vocabularies so script authors can spot typos.

## Examples

    iex> ALLM.Providers.Fake.Script.detect_shape([{:text, "hi"}, {:finish, :stop}])
    {:spec31, [{:text, "hi"}, {:finish, :stop}]}

    iex> ALLM.Providers.Fake.Script.detect_shape([{:ok, %{output_text: "hi"}}])
    {:harness, [{:ok, %{output_text: "hi"}}]}

    iex> ALLM.Providers.Fake.Script.detect_shape([])
    {:spec31, []}

# `fold_to_response`

```elixir
@spec fold_to_response([spec31_entry() | harness_adapter_entry()]) ::
  ALLM.Response.t() | {:error, ALLM.Error.AdapterError.t()}
```

Fold a list of script entries into a single `%ALLM.Response{}`.

Handles both the spec §31 vocabulary (`:text`, `:tool_call`,
`:tool_call_delta`, `:usage`, `:raw_chunk`, `:finish`, `:error`/2,
`:delay`, `:sleep`) and the harness-shape terminal entries (`{:ok, map}`,
`{:error, reason, opts}`). Harness-shape is one entry per call; the
reducer returns on the first harness entry it sees.

A §31 `{:error, term}` entry short-circuits to
`{:error, %AdapterError{reason: :unknown, message: "scripted error",
cause: term}}`. `{:delay, ms}` / `{:sleep, ms}` call `Process.sleep/1`
so non-streaming callers can still exercise timeout paths. `{:raw_chunk,
_}` is ignored (raw chunks have no place on `%Response{}`).

`{:usage, map}` calls `struct!(ALLM.Usage, map)` with last-write-wins
semantics — passing an unknown field name (e.g., `:prompt_tokens`) raises
`KeyError` at fold time; use the canonical field names from
`ALLM.Usage` (spec §5.9a).

Returns `%ALLM.Response{}` on success (NOT `{:ok, %Response{}}` — the
`{:ok, _}` wrapping is applied at the `ALLM.Providers.Fake.generate/2`
boundary in sub-phase 4.2), or `{:error, %AdapterError{}}` on
script-defined failure.

## Examples

    iex> ALLM.Providers.Fake.Script.fold_to_response([{:text, "hi"}, {:finish, :stop}])
    %ALLM.Response{output_text: "hi", finish_reason: :stop, tool_calls: [], usage: %ALLM.Usage{}}

# `interpret`

```elixir
@spec interpret(spec31_entry() | harness_stream_entry()) :: [ALLM.Event.t()]
```

Translate a single script entry into a list of `ALLM.Event` values.

Called per-entry by `ALLM.Providers.Fake.stream/2`. `{:delay, ms}` and
`{:sleep, ms}` return `[]` — the stream runner handles the sleep directly
before emitting the next event, so `interpret/1` has no events to yield.

`:text_completed` is NOT emitted here. Whether to emit it depends on
whether any `:text` was seen earlier in the same call; that state lives in
the `Stream.resource/3` accumulator, not here. `interpret({:finish, _})`
returns just `[{:message_completed, _}]`; the stream runner prepends
`:text_completed` when appropriate. Revisit in sub-phase 4.3 if the
orchestration needs change.

## Deprecation

`{:sleep, ms}` is a deprecated alias for `{:delay, ms}` (spec §31). Passing
a `{:sleep, _}` entry triggers a one-time `Logger.warning/1` per BEAM
lifetime (dedup via `:persistent_term`). Deletion target: v0.3.

# `validate!`

```elixir
@spec validate!(keyword()) :: :ok
```

Validate `adapter_opts` at the Fake boundary.

Guards (in order):

1. Mixing `:script` and `:scripts` raises `ArgumentError` (spec §31).
2. `:script` must be a list (of entries).
3. `:scripts` must be a list of lists (each inner list is one call's script).
4. `:stream_script` must be a list of lists.
5. `:script_cursor`, when present, must be a pid or `nil`.

Returns `:ok` on success.

Users may call this directly on their `adapter_opts` for construction-time
feedback (Non-obvious Decision #5 in the Phase 4 design doc) — Fake itself
calls it at the first adapter invocation.

---

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