Skip to main content

skill.md

# adze

Structural Elixir refactoring + outline-first reading. Built on Sourceror
and Igniter. The tool does the mechanical work; you decide what to do;
the compiler catches mistakes.

## When to reach for adze

| You're about to... | Use this instead |
| --- | --- |
| Read an Elixir file > 200 lines | `mix adze ls --file PATH` first, then read only the line range you need |
| Multi-edit a `defmodule` rename across the project | `mix adze rename! --from Old --to New` |
| Copy a function + its private helpers into a new module by hand | `mix adze extract! --file ... --definition fun/N --module New.Module` |
| Reorder defs inside a module with multi-edit | `mix adze mv! --file ... --definition fun/N --before other/M` |
| Grep for callers of `Mod.fun` across the project | `mix adze find-callers --target Mod.fun/N` |
| Convert a `def` to `defp` after eyeballing call sites | `mix adze extract-private! --file ... --definition fun/N` |
| Eyeball a module's `alias`/`import`/`use` clauses | `mix adze aliases --file PATH` |

If none of these match, fall back to standard Read/Edit/Grep.

## Pre-flight

Run all commands from the project root (not via `--mix-root`).

```bash
mix adze ls --file lib/my_app/router.ex
```

## Read-only ops (safe, no-prompt)

### `ls` — file skeleton (alias: `outline`)

```bash
mix adze ls --file lib/my_app/router.ex
mix adze ls --file lib/my_app/router.ex --format json
```

Returns every top-level definition with line ranges. Legend:

- `defmodule Name   L1–540` — module
- `  foo/2   L42–58` — public def
- `p bar/1   L60` — private def (`defp`)
- `m macro_name/1   L75–90` — `defmacro` (suffix `[private]` for `defmacrop`)
- `g guard_name/1   L95` — `defguard` (suffix `[private]` for `defguardp`)
- `d delegated/2   L100` — `defdelegate`
- `@spec :foo   L40` — type spec
- `alias Module   L5` — directive

Then use your file-read tool on just the line range you need instead of
pulling the whole file.

### `deps` / `ls-deps` — intra-module call graph

```bash
mix adze deps --file lib/my_app/router.ex
mix adze deps --file lib/my_app/router.ex --definition dispatch/2
mix adze ls-deps --file lib/my_app/router.ex --definition dispatch/2
```

`deps` shows caller → callees for edges within the same module.
Pipes (`x |> foo()`) and captures (`&foo/2`) are handled.

`ls-deps` walks the transitive tree from one def (DFS). Revisits are
marked `↺`. Use it to see "what does this function actually touch."

### `ls-extract` — closure preview

```bash
mix adze ls-extract --file lib/my_app/router.ex --definition dispatch/2
```

The target def + every `defp` whose every same-module caller is in the
closure. Run this before `extract` to see what would move.

### `aliases` — directive listing

```bash
mix adze aliases --file lib/my_app/router.ex
```

Per-module list of every `alias` / `import` / `require` / `use`.
Group-form `alias Foo.{A, B}` is expanded.

### `find-callers` — project-wide reference walker

```bash
mix adze find-callers --target MyApp.Foo.bar/2
mix adze find-callers --target MyApp.Foo.bar          # any arity
```

Walks every project source for qualified calls, pipe variants, and
`&Mod.fun/n` captures. Resolves per-file alias tables (including
`alias Foo, as: F` and brace-form). **Won't find:** unqualified calls
via `import`, dynamic `apply/3`, string-literal mentions.

## Write ops — dry-run first, then `!`

**Rule:** dry-run (no `!`) → read diff → bang (`!`) → `mix compile --warnings-as-errors`.

### `mv` / `mv!` — reorder within a module

```bash
mix adze mv  --file lib/my_app/router.ex --definition dispatch/2 --before init/1
mix adze mv! --file lib/my_app/router.ex --definition dispatch/2 --before init/1
```

Moves the whole logical definition group (leading comments + `@spec` /
`@doc` / `@impl` / `@deprecated` / `@since` + all clauses) to just
before `--before`. Same module only.

### `extract` / `extract!` — cut into a new module file

```bash
mix adze extract  --file lib/my_app/router.ex --definition dispatch/2 --module MyApp.Dispatcher
mix adze extract! --file lib/my_app/router.ex --definition dispatch/2 --module MyApp.Dispatcher
```

Cuts the target def + its `defp` closure into `lib/my_app/dispatcher.ex`
(path derived from `--module`; pass `--path` to override). Rewrites
external callers across the project.

**Before `extract!`:**

1. `use` / `import` / `require` are **not** copied — they appear in
   `dropped_directives` in the dry-run. A missing one is a compile
   error you can fix after.
2. If the target file already exists, it errors. Pick a different name
   or delete the file first.
3. If the same def name exists in multiple `defmodule`s in the source
   file, pass `--from-module Foo.Mod` to disambiguate.

### `rename` / `rename!` — module-wide rename

```bash
mix adze rename  --from MyApp.Old --to MyApp.New
mix adze rename! --from MyApp.Old --to MyApp.New
```

Updates `defmodule`, every `alias` / `use` / `import` / `require`, all
call sites, the test module, string-literal references, and moves the
file to its canonical location.

If `rename` returns `surviving_references`, inspect each — fix real ones
by hand, pass `--force` for false positives, then re-run.

### `extract-private` / `extract-private!` — flip `def` → `defp`

```bash
mix adze extract-private  --file lib/my_app/router.ex --definition helper/2
mix adze extract-private! --file lib/my_app/router.ex --definition helper/2
```

Runs `find-callers` first. If zero external callers, flips `def` →
`defp` (or `defmacro` → `defmacrop`, `defguard` → `defguardp`).
Refuses on `defdelegate`. Inherits `find-callers`' blind spots.

## Workflow recipes

### Mapping an unfamiliar codebase

1. `mix adze ls --file <file>` — get the skeleton
2. Read only the line range for the function you care about
3. `mix adze find-callers --target Mod.fun/N` — see where it's used
4. `mix adze ls-deps --file ... --definition fun/N` — see what it touches

### Splitting a fat module

1. `mix adze ls-extract --file ... --definition target/N` — preview the closure
2. `mix adze extract --file ... --definition target/N --module New.Module` — dry-run
3. Read `source_diff`, `target_content`, `caller_diffs`, `dropped_directives`
4. `mix adze extract! ...` — write
5. `mix compile --warnings-as-errors` — fix any missing directives

### Renaming a module

1. `mix adze rename --from Old --to New` — dry-run
2. If `surviving_references`: fix real ones by hand or `--force` for false positives
3. `mix adze rename! --from Old --to New` — write
4. `mix compile --warnings-as-errors`

### Privatizing an over-exposed helper

1. `mix adze find-callers --target Mod.fun/N` — confirm no external callers
2. `mix adze extract-private! --file ... --definition fun/N` — flip

## Important notes

- **All read ops are pure** — side effects only in `!` variants
- **Every op accepts `--format json`** — text (default) is optimized for LLMs reading inline
- **Runs as a Mix task, not a binary** — needs `Mix.Project.config()` loaded
- **Always compile after writes** — `mix compile --warnings-as-errors` catches what adze can't (imports, dynamic dispatch)
- **`find-callers` blind spots** — unqualified `import` calls, `apply/3`, string-literal module names
- **`:adze, :include_attrs` config** — teach adze about custom attachable attrs (e.g. `@job`, `@decorate`)

## Proactive usage

**Before reading any Elixir file over 200 lines, run `mix adze ls --file PATH` first.** Read only the line range you need.

**When the user asks to split or extract from a module,** run `mix adze ls-extract` to find the natural closure, then `mix adze extract!` to execute.

**When you're about to multi-edit a rename across files,** use `mix adze rename!` instead — it handles aliases, call sites, test modules, and file moves.

**When you see a public function that looks like it should be private,** run `mix adze find-callers` to confirm, then `mix adze extract-private!` to flip.

**After any adze write op, always run `mix compile --warnings-as-errors`.** The compiler is your safety net.