# Pitfalls

Compile-time traps that are easy to miss when deriving tools from `@spec` and
`@doc`. Release behaviour is covered in [`design.md`](design.md); the type subset and runtime
boundaries are covered in [`design.md`](design.md) and
[`number-handling.md`](number-handling.md).

## `@spec` carries no parameter names

`@spec f(String.t())` is a value domain, not a record. The schema needs property
names, so the library requires annotated arguments:

```elixir
@spec get_weather(city :: String.t()) :: ...
```

The quoted AST for that argument is `{:"::", _, [{:city, _, nil}, type]}`, so the
name survives compilation. An unannotated argument can also take a name from a
`@doc` parameter bullet; otherwise it falls back to the head variable, then
`argN`, with a compile-time warning.

## `@spec` is an accumulative attribute

`Module.get_attribute(mod, :spec)` contains specs for earlier functions too:

```elixir
@spec get_weather(String.t()) :: ...
@spec two_specs(integer()) :: ...
```

The library filters by `{name, arity}` and warns when a function has several
specs; overloads cannot be represented as one JSON Schema.

## `@doc` is single-valued and consumed immediately

`@doc` is not an accumulating attribute. Reading it later can attach the last
function's text to every tool, or return `nil`. The library captures it from
`@on_definition`, before the attribute is reset; `@doc false` opts a function
out.

## JSON Schema `integer` accepts `1.0`

Draft 2020-12 defines `integer` as a number with a zero fractional part, so a
decoder can produce `1.0`. `ToolCall.Coerce` truncates integral floats and
rejects fractional ones.

## `float()` parameters can receive integers

`{"type":"number"}` accepts `2`, while `is_float(2)` is false. `ToolCall.Coerce`
widens integers to floats before dispatch.

## JSON numbers Elixir cannot represent

`1e309` is valid JSON but not a finite Erlang float; the decoder raises before
the library sees it. The schema cannot prevent this. See
[`number-handling.md`](number-handling.md).

## `@on_definition` fires for private functions and every clause

The hook sees private helpers and each clause of a multi-clause function. The
library ignores non-`def` callbacks and keeps the first `{name, arity}` entry.

## `@spec` names are documentation, not syntax

The compiler allows duplicate and placeholder parameter names:

```elixir
@spec f(city :: String.t(), city :: integer()) :: String.t()
@spec g(_ :: String.t(), _ :: integer()) :: String.t()
```

The library renames duplicates (`city_2`) and falls back from `_` to the head
variable or `argN`, with a warning. Two arities of one function share a tool
name; only the first is exposed.

## `@doc` metadata never reaches the `:doc` attribute

Metadata-only docs such as `@doc deprecated: "use bar/1"` do not reach the
attribute read by `@on_definition`. They also live in the `Docs` chunk, which a
stripped release removes. A tool declared that way falls back to its function
name; write text as well if the model should see a description.

## `@typedoc` is consumed before `@before_compile`

`@typedoc` is consumed by the following `@type` before `@before_compile` runs,
so the phase-one IR cannot see it. ToolCall recovers docs-visible `@type` and
`@opaque` text in `@after_compile` from the just-compiled `Docs` chunk and
stores the enriched literals in a companion module. It is only a fallback
parameter description: a `@doc` `## Parameters` bullet or a `@tool_param`
description wins. Local alias chains are followed to the nearest documented
type, resolving current-module references inside alias bodies; nested uses such
as `list(address())` do not inherit the typedoc.

`@typep` is different: Elixir discards its typedoc at definition time, so no
docs entry exists to recover. A private type is still resolved for the schema,
but it never contributes a description.

## Stale typedoc companions are inert and cleaned by a compile or task

The companion beam is not registered in Mix's compile manifest. Source-file
companions carry a `.source` sidecar with their source path; every ToolCall
compile prunes companions whose source file is gone. If no ToolCall module
compiles after deleting a source file, or a module was removed from an existing
file, run `mix tool_call.clean` before assembling a release. The task also
scans companion beams directly, using the parent beam and a live source from
either the `.source` sidecar or the beam's `compile_info.source`; a stripped
`compile_info` leaves only the parent-beam check.

The compile-generation token makes any remaining stale companion unusable by
the new module, so it falls back to phase-one data instead of serving stale
descriptions. A companion generated by a ToolCall build that predates the
`__tool_call_companion__/0` marker is treated as foreign, so upgrade with one
`mix clean` or ToolCall warns and skips typedoc enrichment rather than
overwriting it.

## A missing `Docs` chunk is not always a problem

`docs: false` intentionally omits the chunk. ToolCall skips typedoc silently in
that compile and only warns about a missing chunk when docs are enabled. An
undecodable or structurally invalid `Docs` chunk is reported as corrupt and
falls back to phase-one descriptions.

## `@on_definition` accumulates

Unlike `@doc`, `@on_definition` is an accumulating attribute. A `use ToolCall`
does not replace another library's definition hook; both callbacks fire,
including for generated functions such as `__tools__/0`.

## Remaining design boundaries

Some limits are intentional: nested/remote/provider typedoc inheritance,
`@typep` typedoc recovery, overloaded specs, two arities of one function,
tuple defaults, JSON numbers outside Erlang's finite range, `@doc` metadata-only
entries and type-provider typedoc. See the Design boundaries section in
[`design.md`](design.md).
