# CodeStory

Every codebase has a story. CodeStory lets you read it. Drop `CodeStory.tell()` into a function and see the narrative unfold: which functions are called, with what arguments (by name and value), and what they return — rendered as a nested call tree.

**Use cases:**

- Joining a new codebase and understanding how it actually works
- Tracing call flow before refactoring
- Spotting redundant or unexpected function calls
- Exposing your code's vocabulary and catching ubiquitous language mismatches
- Debugging by seeing exactly where data goes wrong

## Requirements

- **Elixir 1.15+**
- **OTP 27+** — CodeStory uses the session-based `:trace` module introduced in
  OTP 27. On older OTP releases, starting a trace will fail.

## Installation

Add `code_story` to your dependencies in `mix.exs`:

```elixir
defp deps do
  [{:code_story, "~> 0.2.0", only: :dev}]
end
```

Install it as `only: :dev` so that any `CodeStory.tell()` calls you forget to
remove fail to compile in production rather than shipping.

To track the development version instead, point at the repository:

```elixir
defp deps do
  [{:code_story, github: "angeleah/code_story", only: :dev}]
end
```

Or if you've cloned it locally, point to the path on disk:

```elixir
defp deps do
  [{:code_story, path: "../code_story", only: :dev}]
end
```

Then fetch the dependency:

```bash
mix deps.get
```

No `require`, no `use`, no macros. Just `CodeStory.tell()` and `CodeStory.stop()`.

## Usage

The quickest way is to **wrap the call you want to understand**. The trace prints,
tracing cleans up on its own (no `stop()`), and your result flows through unchanged:

```elixir
invoice = CodeStory.tell(fn -> process_order(params) end)
```

This is ideal for unfamiliar code: you know the entry point even when you don't
know where the flow ends. Wrapping is always safe — if tracing can't run for any
reason, your function still runs and returns normally.

Prefer to bracket a region by hand — a LiveView handler, or a span across several
statements? Use the manual pair: `CodeStory.tell()` before, `CodeStory.stop()`
after (`stop()` is what prints):

```elixir
def handle_request(params) do
  CodeStory.tell()
  result = process_order(params)
  CodeStory.stop()
  result
end
```

Need the tree as data instead of a printed trace? See `CodeStory.narrate/2`.

This outputs a nested call tree to the terminal:

```
--- CodeStory Trace ---
MyApp.Orders.process_order(params: %{customer_id: 7, items: [...]})
  MyApp.Orders.validate_item(item: %{sku: "A1", qty: 2}) => :ok ×3 (varies)
  MyApp.Orders.calculate_total(items: [...]) => 29.97
  MyApp.Repo.insert!(changeset: #Ecto.Changeset<...>) => %MyApp.Invoice{id: 42}
=> MyApp.Orders.process_order returned %MyApp.Invoice{id: 42}
--- End Trace ---
```

Each call renders as a compact **inline signature** — `Mod.fun(name: value, …) => return`
on one line — so the trace reads as one dense indented tree, with indentation showing call
depth. A call whose assembled line would exceed the `:width` budget (default `100`) falls
back to a **stacked layout** instead: the function name, one argument per line, then the
return. So a trace naturally mixes inline (small calls) and stacked (struct-heavy ones).

A few things happen by default to keep the story readable:

- **Repeated calls fold.** The three `validate_item` calls collapse into one node marked `×3` — or `×3 (varies)` when the calls share a function but differ in their arguments (a single representative call is shown). Turn this off with `fold_repeats: false`.
- **Infrastructure stays at the boundary.** A call into your Ecto repo appears as a single node rendered with its **known Ecto parameter names** — `Repo.insert!(changeset: …)`, `Repo.get!(queryable, id)`, `Repo.aggregate(queryable, aggregate)` — while the repo's internal Ecto plumbing is hidden. Turn this off with `auto_boundary: false`.
- **Ecto values read cleanly.** A schema struct drops its `__meta__`/`NotLoaded` bookkeeping (so it reads `%Order{id: 12, status: "paid", …}`), and an `Ecto.Query` argument shows as `#Ecto.Query<Schema>` rather than the full query dump. No Ecto dependency — detection is purely string-shaped.

Only your project's own functions appear in the trace. Standard library calls, dependency code, framework-generated functions (like `__struct__/0`, `__changeset__/0`), and CodeStory itself are filtered out automatically.

## Options

All options are passed to `CodeStory.tell/1`:

```elixir
CodeStory.tell(detail: :outline)
CodeStory.tell(detail: :novel, output: :file)
CodeStory.tell(show_args: false, output: :both)
```

- **`detail`** — how much of the story to tell. Default: `:short_story`.
  - `:outline` — function names and argument names only. No values, no returns. Great for seeing the shape of a call flow, spotting boundary crossings, and finding redundant calls.
  - `:short_story` — names, truncated values, and returns. The default — enough detail to follow the plot without getting lost in the data.
  - `:novel` — names with complete, untruncated values and returns. Every detail, nothing elided. Use when you need to see the full picture.

- **`show_args`** — show argument names alongside values. Default: `true`.
  Set to `false` to show values only.

- **`output`** — where to write the trace. Default: `:terminal`.
  `:file` writes to `code_story_trace.log` in your project root (ANSI codes stripped).
  `:both` writes to terminal and file.

  > **Add `code_story_trace.log` to your `.gitignore` before using `:file`.**
  > A trace records real argument and return values, so tracing code that
  > handles passwords, API keys, tokens, or personal data writes those values
  > to the log in plaintext. Treat the file as sensitive and delete it when
  > you're done reading it.

- **`auto_boundary`** — treat Ecto repos as _boundary modules_: a repo call
  (e.g. `Repo.get!`) shows as a single node with its arguments and return, but the
  repo's internal Ecto plumbing is hidden. Default: `true`. Set to `false` to
  trace repo internals.

- **`fold_repeats`** — collapse consecutive sibling calls to the same function
  into one node marked `×N` (or `×N (varies)` when the calls share a function but
  differ). Default: `true`. Set to `false` to show every call.

- **`depth`** — cap how many levels the trace nests. A positive integer
  (`depth: 1` shows the entry call only; `depth: 2` adds its direct children, and
  so on); below the cap a node's interior is replaced by a `… (N more levels)`
  marker. Default: `:infinity` (no limit).

- **`width`** — the line-width budget for the compact inline signature
  `Mod.fun(name: value, …) => return`. A call whose assembled line fits within
  `width` renders inline; a longer one falls back to the stacked layout (name,
  one argument per line, return). Default: `100`. Raise it for a wide terminal,
  lower it for a strict slide, or pass `:infinity` to force every call inline.

## How It Differs from `dbg/2`

|                   | `dbg/2`                                | CodeStory                                                  |
| ----------------- | -------------------------------------- | ---------------------------------------------------------- |
| **Scope**         | Single expression or pipeline          | Span of execution between tell/stop                        |
| **What it shows** | Every intermediate value in a pipeline | Only user-defined function calls (filters out stdlib/deps) |
| **Identity**      | Shows code expressions                 | Shows function names with named arguments                  |
| **Purpose**       | Debug a specific value                 | Hear the story — understand call flow                      |
| **Output**        | Per-expression, inline                 | Buffered, dumped as one cohesive block                     |

## How It Works

1. **Module detection** — reads your `mix.exs` app name and finds all your project's modules
2. **Argument name extraction** — reads Elixir debug info from BEAM files to recover original parameter names, scanning across all function clauses to find the best names
3. **Erlang tracing** — sets up trace sessions on the calling process for your modules
4. **Tree building** — a collector process receives trace events and builds a nested call tree
5. **Formatted output** — the tree is rendered with indentation and ANSI colors, then dumped as one block

## Color Scheme

Terminal output uses ANSI colors for readability:

- **Header/footer** (`--- CodeStory Trace ---`): cyan
- **Function names**: blue
- **Argument names**: yellow
- **Argument values**: default terminal color
- **Return values**: green
- **Fold marker** (`×N`): magenta

The semantic colors render **bold**, so the trace stays legible on both light and dark
terminals (and projectors). Meaning never rests on color alone — structure, `=>`, `name:`,
`×N`, and indentation carry it — so bold is purely a readability bump, not a dependency.

## Limitations (v1)

- **Single process only** — traces the calling process. Calls in spawned Tasks, GenServers, etc. are not captured.
- **Modules detected at tell time** — hot-reloaded modules mid-trace won't be traced.
- **Dev only** — installed with `only: :dev`, so leftover `CodeStory.tell()` calls fail to compile in prod.
- **One trace per process** — calling `tell()` while a trace is already active warns and returns an error.

## License

Copyright 2026 Angeleah Daidone

Licensed under the [Apache License, Version 2.0](LICENSE). You may not use this
project except in compliance with the License.
