# `ToolCall.Typespec`

Converts a quoted typespec AST (aliases already expanded) into `ToolCall.IR.Type`.

This module is deliberately total: unknown shapes degrade to `:any` rather
than raising, so that adding a new typespec construct to a codebase never
breaks compilation of unrelated tools.

# `open_object?`

```elixir
@spec open_object?(ToolCall.IR.Type.t() | nil) :: boolean()
```

True when the tree contains an object whose keys the spec does not fix.

Strict mode closes every object with `additionalProperties: false`, so an open
one reaches the model as an object that accepts no key at all.

# `to_type`

```elixir
@spec to_type(term(), [module()]) :: ToolCall.IR.Type.t()
```

Maps a quoted type to a `ToolCall.IR.Type`.

Aliases must already be expanded (`Macro.expand/2`), otherwise remote types
such as `String.t()` arrive as `{:__aliases__, ...}` and degrade to `:any`.

Built-in providers resolve `String`, `Keyword`, `Path` and the calendar types
and are used by default. Passing `providers` explicitly replaces that default;
`use ToolCall, types: [...]` prepends the built-ins before the user list. A
provider must return a well-formed IR node: an unknown kind, a non-string or
uncompilable pattern, a malformed transform, enum values, bounds, bound
ordering, array minimums, formats, nullability, field metadata, or malformed
`fields`/`items`/`variants`/`additional` raises at compile time.

# `tuple?`

```elixir
@spec tuple?(ToolCall.IR.Type.t() | nil) :: boolean()
```

True when the tree contains a tuple, which strict mode cannot express.

OpenAI rejects `prefixItems`, so the emitter can only keep the arity.

# `unrepresentable?`

```elixir
@spec unrepresentable?(ToolCall.IR.Type.t()) :: boolean()
```

True when the tree contains a domain JSON cannot represent at all.

`:any` is not unrepresentable: it means "unknown", and JSON can carry any
value for it. Only `:unsupported` (functions, pids, refs) is.

# `untyped?`

```elixir
@spec untyped?(ToolCall.IR.Type.t() | nil) :: boolean()
```

True when the tree has no JSON type the model can constrain against.

This is broader than `unrepresentable?/1`: `:any` and open objects do have a
JSON encoding, but they cannot be typed in the strict schema subset.

# `validate_tool!`

```elixir
@spec validate_tool!(ToolCall.IR.Tool.t()) :: ToolCall.IR.Tool.t()
```

Validates a runtime `ToolCall.IR.Tool` and returns a copy with prepared
parameters.

Parameter types are validated through `validate_type!/1`, so hand-built
patterns receive a compiled `pattern_regex`; tool and parameter metadata are
checked as well.

# `validate_type!`

```elixir
@spec validate_type!(ToolCall.IR.Type.t()) :: ToolCall.IR.Type.t()
```

Validates a runtime `ToolCall.IR.Type` and returns a copy with compiled
`pattern_regex`.

Use this for hand-built types before passing them to `ToolCall.Coerce` or
rendering them with `ToolCall.Emit`.

---

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