Skip to main content

CONSUMING.md

# Consuming Descripex Libraries

A guide for AI agents and developers who want to **discover and call** the API of a
descripex-powered Elixir library — without reading source or parsing docs.

## Why Descripex Libraries Are Worth Finding

Most Elixir libraries require you to read source code, parse docs, and guess at return shapes. A descripex-powered library **tells you its entire API contract in structured data** — params, types, defaults, errors, return shapes, composition hints — through three function calls.

No doc parsing. No source diving. No guessing.

```elixir
MyLib.describe()                     # what modules exist?
MyLib.describe(:funding)             # what functions does it have?
MyLib.describe(:funding, :annualize) # tell me everything about this function
```

Every contract is **compile-time validated** — if the library compiles, the metadata aligns with actual function signatures and arities. You can trust the structure and focus your effort on behavior.

## Detecting Descripex Support

A library uses descripex if any of these are true:
- `function_exported?(SomeModule, :__api__, 0)` — module-level introspection
- `function_exported?(MyLib, :describe, 0)` — library-level discovery
- A module's `@moduledoc` contains an "API Functions" table

**When you detect it, use it.** The structured metadata is always more reliable than parsing `@doc` strings.

## Discovery Workflow

Three levels, each one step deeper. Start broad, drill into what you need.

### Level 1: What Modules Exist?

```elixir
MyLib.describe()
```

```elixir
[%{
  module: MyLib.Funding,       # full module atom
  short_name: :funding,        # use this for Level 2/3 — shorter, friendlier
  namespace: "/funding",       # URL grouping hint (or nil)
  description: "...",          # @moduledoc text (or nil)
  function_count: 3,           # how many public API functions
  annotated?: true             # true = full contracts available
}]
```

`annotated?: true` means full contracts (params, returns, errors). `false` means only basic `@doc`/`@spec` info — still useful, just less structured.

### Level 2: What Functions Does It Have?

```elixir
MyLib.describe(:funding)
```

```elixir
[%{
  name: :annualize,            # function name (atom)
  arity: 2,                    # max arity
  defaults: 1,                 # number of optional args — callable with 1 or 2 args
  description: "...",          # one-line description
  spec: "annualize(...) :: float()"  # typespec string (or nil)
}]
```

Scan this to find the function you need. Then drill in.

### Level 3: Full Function Contract

```elixir
MyLib.describe(:funding, :annualize)
```

This is where it pays off — everything you need to call the function correctly:

```elixir
%{
  name: :annualize,
  arity: 2,
  defaults: 1,
  description: "Annualize a per-period funding rate to APR.",
  spec: "annualize(number(), pos_integer()) :: float()",
  params: %{
    rate: %{kind: :value, description: "Per-period funding rate as decimal",
            schema: %{"type" => "number"}},        # JSON Schema, derived from @spec
    period_hours: %{kind: :value, default: 8, description: "Hours per period",
                    schema: %{"type" => "integer"}}
  },
  opts: %{                     # keyword options (or nil if none)
    precision: %{type: :integer, default: 2, description: "Decimal places",
                 schema: %{"type" => "integer"}}   # derived from the opt's type:
  },
  returns: %{type: :float, description: "Annualized percentage rate"},
  returns_example: 10.95,      # a concrete value you can expect back
  errors: [invalid_period: "Period must be > 0"],
  composes_with: [:normalize_rate]  # what to call next
}
```

You now know the param order, which are optional, what types to pass, what comes back, what can go wrong, and what to chain with. **You can call this function correctly without reading a single line of source code.**

### Reading the Contract

**`params`** — Positional arguments. Pass them in order. Check `kind`:
- `:value` — you provide this directly (a number, string, config)
- `:exchange_data` — must be fetched from an external source first (the param may include a `source` hint telling you where to get it)

**`opts`** — Keyword options. Pass as last argument: `annualize(rate, period, precision: 4)`.

**`schema`** — A JSON Schema map present on most params/opts, giving the wire type for JSON/MCP callers. Derived automatically from the function's `@spec` (params) or the opt's `type:` (opts), or from an explicit `schema:` declaration. Absent only for types a typespec can't express (e.g. `term()`, tuples). Elixir callers can ignore it and use `kind`/`type`/`default`.

**`defaults`** — Number of trailing params with defaults. If `arity: 3, defaults: 1`, you can call with 2 or 3 args.

**`errors`** — Known error cases the function may return or raise; treat these as contract hints and follow the library's actual return conventions.

**`composes_with`** — Other functions in the same module that chain well with this one. Follow the chain to build pipelines without guessing.

**`returns_example`** — A concrete value showing what the output looks like. Use this to understand the shape before you call.

## Combining Manual @doc with api()

`api()` generates both human-readable `@doc` text and machine-readable `@doc hints:` metadata. These live in **separate slots** in Elixir's compiled BEAM docs (element 4 and element 5 of the docs tuple) — they never collide.

This means you can write a manual `@doc` **after** `api()` to provide custom prose while keeping the structured metadata:

```elixir
# api() writes hints metadata (slot 5) AND generated @doc text (slot 4)
api(:imbalance!, "Calculate orderbook bid/ask imbalance (raises on error).",
  params: [
    orderbook: [kind: :exchange_data, description: "Orderbook data"],
    depth: [kind: :value, default: 10, description: "Depth levels"]
  ],
  returns: %{type: :float, description: "Imbalance ratio"}
)

# Manual @doc AFTER api() — overwrites only slot 4 (prose), hints in slot 5 survive
@doc "Bang variant of `imbalance/2`. Returns the float directly or raises on error."
@spec imbalance!(map(), pos_integer()) :: float()
def imbalance!(orderbook, depth \\ 10), do: ...
```

Result: the function gets **both** the rich human-friendly `@doc` text AND the full machine-readable `hints` contract. Best of both worlds.

**Important:** The manual `@doc` must come **after** `api()` — Elixir uses last-wins for `@doc` text. If placed before, `api()`'s generated text overwrites it.

## Alternative Entry Points

### Direct Module Introspection

When you know the exact module, skip the top-level and go direct:

```elixir
MyLib.Funding.__api__()
# => [%{name: :annualize, arity: 2, defaults: 1, spec: "...", hints: %{...}}, ...]

MyLib.Funding.__api__(:annualize)
# => %{name: :annualize, arity: 2, defaults: 1, spec: "...", hints: %{...}}
```

The `hints` map has the same fields as Level 3 (`params`, `opts`, `returns`, `returns_example`, `errors`, `composes_with`, `description`).

**`__api__/0` is runtime-enriched; the BEAM doc chunk is not.** `__api__/0` fills
`hints.params.<name>.schema` / `hints.opts.<name>.schema` from the function's `@spec` and
declared `type:` at runtime. The compile-time doc chunk (`Code.fetch_docs/1` → `meta[:hints]`)
is the raw declared surface and carries no spec-derived schemas, so the two diverge on
`:schema`. This is intentional. If you cross-check the two surfaces for equality (e.g. to
detect `api()` misattachment), normalize **both** with `Descripex.normalize_for_doc_compare/1`
first — it strips every `:schema` key so the comparison doesn't false-positive on the
injected schema:

```elixir
Descripex.normalize_for_doc_compare(MyLib.Funding.__api__(:annualize).hints) ==
  Descripex.normalize_for_doc_compare(meta_hints)
```

### Get the Module List

```elixir
MyLib.__descripex_modules__()
# => [MyLib.Funding, MyLib.Risk]
```

### Without a Top-Level Discoverable

Use `Descripex.Describe` directly — same three levels, but you pass the module list:

```elixir
Descripex.Describe.describe([MyLib.Funding, MyLib.Risk])
Descripex.Describe.describe([MyLib.Funding, MyLib.Risk], :funding)
Descripex.Describe.describe([MyLib.Funding, MyLib.Risk], :funding, :annualize)
```

### Manifest (Batch/Offline)

Grab the entire library's API as a JSON-serializable map:

```elixir
Descripex.Manifest.build([MyLib.Funding, MyLib.Risk])
```

```elixir
%{
  version: "1.0",
  generated_at: "2025-01-01T00:00:00Z",
  modules: [%{
    module: "MyLib.Funding",     # strings in manifest (JSON-friendly)
    namespace: "/funding",
    description: "...",
    functions: [%{
      name: "annualize",        # strings (not atoms)
      arity: 2,
      defaults: 1,
      signature: "annualize(rate, period_hours)",
      description: "...",
      spec: "annualize(number(), pos_integer()) :: float()",
      hints: %{...}             # same shape as __api__ hints
    }]
  }]
}
```

Note: Manifest uses **strings** for module/function names. `__api__` and `describe` use **atoms**.

For an **offline / batch** export to a file (CI, agent toolchains), use the Mix task instead of
calling `Manifest.build/1` yourself:

```bash
mix descripex.manifest MyLib.Funding MyLib.Risk   # writes api_manifest.json
mix descripex.manifest --app my_app               # auto-discover annotated modules in an app
mix descripex.manifest --pretty -o tools.json MyLib.Funding
```

### MCP Tool Definitions

If you host the library behind the Model Context Protocol, turn its annotated modules straight
into MCP tool definitions — no hand-written schemas:

```elixir
Descripex.MCP.tools([MyLib.Funding, MyLib.Risk])
# => [%{
#   name: "funding__annualize",          # "<short_module>__<function>"
#   description: "Annualize a per-period funding rate to APR.",
#   inputSchema: %{type: "object", properties: %{...}, required: [...]}
# }, ...]
```

`inputSchema` is a JSON Schema assembled from the function's `params` and `opts` (the same
schemas you see at Level 3). Pass `name_style: :full` for fully-qualified tool names
(`my_lib_funding__annualize`). Functions without `api()` annotations are skipped.

## Quick Reference

| Want to... | Call |
|------------|------|
| List all modules | `MyLib.describe()` |
| List functions in a module | `MyLib.describe(:funding)` |
| Get full function contract | `MyLib.describe(:funding, :annualize)` |
| Get module list | `MyLib.__descripex_modules__()` |
| Introspect one module directly | `MyLib.Funding.__api__()` |
| Introspect one function directly | `MyLib.Funding.__api__(:annualize)` |
| Get everything as JSON-ready map | `Descripex.Manifest.build(modules)` |
| Export manifest to disk | `mix descripex.manifest MyLib.Funding` |
| Get MCP tool definitions | `Descripex.MCP.tools(modules)` |
| Detect descripex support | `function_exported?(Mod, :__api__, 0)` |