# `PtcRunner.SubAgent.PromptExpander`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/sub_agent/prompt_expander.ex#L1)

Template string expansion with placeholder validation.

Provides functions to:
- Extract placeholders from template strings
- Expand templates by replacing placeholders with values from a context map

## Placeholder Syntax

Placeholders use `{{variable}}` syntax and support nested access with dot notation:

- Simple: `{{name}}`
- Nested: `{{user.name}}` or `{{items.count}}`

## Mustache Sections (Text Mode Only)

The `expand/3` function supports Mustache sections for iterating over lists:

- List iteration: `{{#items}}{{name}} {{/items}}`
- Scalar lists with dot: `{{#tags}}{{.}} {{/tags}}`
- Inverted sections: `{{^items}}No items{{/items}}`

**Note:** Sections are intended for text mode agents where data is embedded directly
in the prompt. For PTC-Lisp mode, use `expand_annotated/2` which returns annotations
like `~{data/var}` and does not support sections (the Data Inventory is flat).

## Examples

    iex> PtcRunner.SubAgent.PromptExpander.expand("Hello {{name}}", %{name: "Alice"})
    {:ok, "Hello Alice"}

    iex> PtcRunner.SubAgent.PromptExpander.expand("User {{user.name}}", %{user: %{name: "Bob"}})
    {:ok, "User Bob"}

    iex> PtcRunner.SubAgent.PromptExpander.expand("Hello {{name}}", %{})
    {:error, {:missing_keys, ["name"]}}

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("Hello {{name}}, you have {{items.count}} items")
    [%{path: ["name"], type: :simple}, %{path: ["items", "count"], type: :simple}]

# `expand`

```elixir
@spec expand(String.t(), map(), keyword()) ::
  {:ok, String.t()} | {:error, {:missing_keys, [String.t()]}}
```

Expand a template by replacing placeholders with values from the context.

Returns `{:ok, expanded_string}` on success, or `{:error, {:missing_keys, keys}}`
if any placeholders cannot be resolved (when `on_missing: :error`).

The context map can use either atom or string keys. Values are converted to
strings using `to_string/1`.

## Options

- `on_missing`: Controls behavior when a placeholder key is missing from the context.
  - `:error` (default) - Returns `{:error, {:missing_keys, [...]}}` if any keys are missing
  - `:keep` - Leaves missing placeholders unchanged in the output (e.g., `"{{name}}"`)

## Examples

    iex> PtcRunner.SubAgent.PromptExpander.expand("Hello {{name}}", %{name: "Alice"})
    {:ok, "Hello Alice"}

    iex> PtcRunner.SubAgent.PromptExpander.expand("Count: {{count}}", %{count: 42})
    {:ok, "Count: 42"}

    iex> PtcRunner.SubAgent.PromptExpander.expand("{{a.b.c}}", %{a: %{b: %{c: "deep"}}})
    {:ok, "deep"}

    iex> PtcRunner.SubAgent.PromptExpander.expand("Hello", %{})
    {:ok, "Hello"}

    iex> PtcRunner.SubAgent.PromptExpander.expand("", %{})
    {:ok, ""}

    iex> PtcRunner.SubAgent.PromptExpander.expand("{{missing}}", %{})
    {:error, {:missing_keys, ["missing"]}}

    iex> PtcRunner.SubAgent.PromptExpander.expand("{{a}} and {{b}}", %{a: "1"})
    {:error, {:missing_keys, ["b"]}}

    iex> PtcRunner.SubAgent.PromptExpander.expand("{{missing}}", %{}, on_missing: :keep)
    {:ok, "{{missing}}"}

    iex> PtcRunner.SubAgent.PromptExpander.expand("{{a}} and {{b}}", %{a: "1"}, on_missing: :keep)
    {:ok, "1 and {{b}}"}

# `expand_annotated`

```elixir
@spec expand_annotated(String.t(), map()) ::
  {:ok, String.t()} | {:error, {:missing_keys, [String.t()]}}
```

Expand a template with annotations showing where substitutions occurred.

Returns an annotated string where substituted values are wrapped with `~{data/...}`
syntax to make it clear which parts came from template variables. This is useful
for debugging to distinguish dynamic values from hardcoded text.

## Examples

    iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("Hello {{name}}", %{name: "Alice"})
    {:ok, "Hello ~{data/name}"}

    iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("Count: {{count}}", %{count: 42})
    {:ok, "Count: ~{data/count}"}

    iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("{{a.b}}", %{a: %{b: "deep"}})
    {:ok, "~{data/a.b}"}

    iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("Hello", %{})
    {:ok, "Hello"}

    iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("{{missing}}", %{})
    {:error, {:missing_keys, ["missing"]}}

# `extract_placeholder_names`

```elixir
@spec extract_placeholder_names(String.t()) :: [String.t()]
```

Extract placeholder names from a template string as a flat list.

This is a convenience wrapper around `extract_placeholders/1` that returns
only the placeholder names as flat strings (e.g., "name", "user.name").

## Examples

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholder_names("Hello {{name}}")
    ["name"]

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholder_names("{{user.name}} has {{count}} items")
    ["user.name", "count"]

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholder_names("No placeholders here")
    []

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholder_names("{{name}} and {{name}}")
    ["name"]

# `extract_placeholders`

```elixir
@spec extract_placeholders(String.t()) :: [%{path: [String.t()], type: :simple}]
```

Extract placeholders from a template string.

Returns a list of unique placeholder structs, each containing:
- `path`: List of strings representing the nested path (e.g., ["user", "name"])
- `type`: Always `:simple` (for backward compatibility, section names are flattened)

## Examples

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("Hello {{name}}")
    [%{path: ["name"], type: :simple}]

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("{{user.name}} has {{count}} items")
    [%{path: ["user", "name"], type: :simple}, %{path: ["count"], type: :simple}]

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("No placeholders here")
    []

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("{{name}} and {{name}}")
    [%{path: ["name"], type: :simple}]

# `extract_placeholders_with_sections`

```elixir
@spec extract_placeholders_with_sections(String.t()) :: [
  PtcRunner.Mustache.variable_info()
]
```

Extract all placeholders with full section information.

Unlike `extract_placeholders/1`, this returns the complete variable structure
including section types and nested fields. Used for signature validation in Phase 3.

## Examples

    iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders_with_sections("{{name}}")
    [%{type: :simple, path: ["name"], fields: nil, loc: %{line: 1, col: 1}}]

    iex> {:ok, [section]} = {:ok, PtcRunner.SubAgent.PromptExpander.extract_placeholders_with_sections("{{#items}}{{name}}{{/items}}")}
    iex> section.type
    :section
    iex> section.path
    ["items"]
    iex> [field] = section.fields
    iex> field.path
    ["name"]

# `extract_signature_params`

```elixir
@spec extract_signature_params(String.t()) :: [String.t()]
```

Extract parameter names from a SubAgent signature string.

Parses the signature and returns a list of parameter names.
Returns an empty list if the signature cannot be parsed.

## Examples

    iex> PtcRunner.SubAgent.PromptExpander.extract_signature_params("(user :string) -> :string")
    ["user"]

    iex> PtcRunner.SubAgent.PromptExpander.extract_signature_params("(name :string, age :int) -> :string")
    ["name", "age"]

    iex> PtcRunner.SubAgent.PromptExpander.extract_signature_params("invalid signature")
    []

# `validate_placeholders!`

```elixir
@spec validate_placeholders!(String.t(), String.t()) :: :ok
```

Raise `ArgumentError` if any `{{placeholder}}` in `prompt` is not a parameter of `signature`.

Returns `:ok` when every placeholder is covered by the signature parameters.

## Examples

    iex> PtcRunner.SubAgent.PromptExpander.validate_placeholders!("Hello {{name}}", "(name :string) -> :string")
    :ok

    iex> PtcRunner.SubAgent.PromptExpander.validate_placeholders!("No placeholders", "(name :string) -> :string")
    :ok

---

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