# Visualize

> Build an interactive "factory floor" map of any codebase as a self-contained HTML visualization backed by JSON graph files in a .visualize/ folder. Use in any project when the user asks to visualize, map, diagram, or explore a codebase, module dependencies, or file relations graphically.

- Skill: `amirrezasalimi/visualize` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add amirrezasalimi/visualize`
- Raw SKILL.md: https://api.skillmd.com/api/skills/amirrezasalimi/visualize/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: amirrezasalimi (https://skillmd.com/u/amirrezasalimi)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/amirrezasalimi/visualize

---


# Codebase Visualizer

## Commands

If the user's message starts with `/visualize`, read `COMMANDS.md` in this skill
directory and follow the matching subcommand. The most important ones:

- `/visualize` — full build
- `/visualize refetch` — re-copy the renderer only, one `copy_path`, touch nothing else
- `/visualize refresh` — re-scan and rewrite JSON only, leave `index.html` alone

A bare request like "visualize this codebase" means a full build.

Produces `.visualize/` inside the target project:

```
.visualize/
  index.html      # copied verbatim from assets/visualizer.html — NEVER authored or edited
  root.json       # top-level graph (always)
  <cluster>.json  # optional nested graphs, MAX 9 (10 json files total)
```

## The one hard rule

**`assets/visualizer.html` is a fixed binary-like artifact. Copy it. Never read it to
"understand" it, never edit it, never regenerate it, never inline data into it.**

It is a pure renderer: it contains zero graph data and loads everything from the JSON
files beside it. Your entire job is producing good JSON.

This holds even when the user asks for visual or behavioural changes. If they ask for
different colours, different grouping, fewer nodes, a different shape — that is almost
always a **data** change: adjust clusters, weights, node counts, or edge kinds in the
JSON. The renderer already derives colour, position, layering, and label priority from
those fields.

Only touch `assets/visualizer.html` if the user *explicitly and unambiguously* asks you
to modify the visualizer/renderer itself. Then edit the file **in the skill directory**
and re-copy it, so every future project benefits. Never edit the copy inside a
project's `.visualize/`; it would be silently overwritten on the next run.

`assets/root.json` in this skill directory is a reference example of the schema. Read
it when you need a concrete shape to imitate. Do not copy it into a project.

`preview/` is a ready-to-serve demo (`index.html` + `root.json`) for checking the
renderer without a real project:

```
python3 -m http.server 8000 -d ~/.agents/skills/visualize/preview
```

It holds **copies**. If the user ever has you change the renderer, re-copy both
`assets/visualizer.html` → `preview/index.html` and `assets/root.json` →
`preview/root.json`, or the preview goes stale.

## Method: scan first, then reason

Do **not** LLM-read every file, and do **not** rely on parsing alone. Mix the two:

**Phase 1 — mechanical scan (cheap, exhaustive).**
Use `terminal` + `grep` to extract raw facts. Never read file bodies here.

- File inventory: `git ls-files | grep -Ev 'node_modules|dist|build|\.lock'`
- Import edges (TS/JS): `grep -rn "^import .* from ['\"]" --include=*.ts --include=*.tsx <src>`
  Also catch `require(`, `import(`, `export * from`, Python `^from|^import`,
  Go `import (`, Rust `^use `.
- Resolve relative specifiers to repo-relative paths. Drop edges to `node_modules`/stdlib.
- `weight` = fan-in count. `kind: "entry"` = zero fan-in, `kind: "hot"` = top decile fan-in.

**Phase 2 — LLM condensation (targeted, small).**
Only now use judgement, and only on what the scan cannot give you:

- Name the clusters. Group by directory first, then merge/rename into meaningful domain
  names (`auth`, `billing`, `inbox`) using file names plus a handful of reads of the
  highest-fan-in files.
- Classify non-import relations the scanner misses: a file that writes to a model,
  emits a queue job, or mutates shared state → edge `kind: "write"`.
- Drop noise edges (barrel `index.ts` re-exports, type-only imports) so the map stays legible.

**Budget rule:** read at most ~15 source files in full. Everything else comes from grep
output. If the repo has >300 relevant files, keep the top ~120 by fan-in for `root.json`
and push the remainder into cluster files.

## Nesting rule

- `root.json` holds clusters and the top ~120 nodes.
- Create a child `<cluster>.json` **only** when a cluster has >25 internal nodes worth
  expanding. Hard cap: 9 child files. Never exceed 10 JSON files total.
- A child uses the identical schema with `parent` set to the cluster id.
- Link parent → child by setting `child: "<file>.json"` on the node that represents the
  cluster. The inspector then shows an "open cluster →" button.

## Schema

```jsonc
{
  "id": "root",
  "label": "project-name",
  "parent": null,                    // cluster id in child files
  "nodes": [
    {
      "id": "inbox-verdict",         // stable, unique, slug-like
      "label": "inbox-verdict.ts",   // shown on the map
      "path": "backend/inbox/inbox-verdict.ts",
      "group": "inbox",              // cluster → district/building + auto colour
      "kind": "core",                // entry | core | hot
      "weight": 3,                   // fan-in; drives label priority + index bars
      "child": "inbox.json"          // optional, only if a child file exists
    }
  ],
  "edges": [
    { "from": "mutate-ticket", "to": "inbox", "kind": "import" },
    { "from": "inbox", "to": "inbox-verdict", "kind": "write" }
  ]
}
```

`group` may be **any** string — colours are auto-assigned from a 7-way palette, so you
never need to match preset names. Edge `kind` is only `"import"` (cyan) or `"write"`
(violet). Keep both `nodes` and `edges` present and non-empty; the renderer shows a
diagnostic screen otherwise.

## Procedure

1. Confirm the target directory and detect the language(s).
2. Run Phase 1 scans. Keep the raw edge list in memory, not on disk.
3. Run Phase 2 condensation: clusters, weights, write-edges, child files.
4. `create_directory` `<project>/.visualize/`.
5. `copy_path` `assets/visualizer.html` → `<project>/.visualize/index.html`.
   **Copy only.** Do not open, read, or modify it.
6. `write_file` `root.json` (+ any child files).
7. Suggest adding `.visualize/` to `.gitignore` unless the user wants it committed.
8. Tell the user the serve command. Do not start a server yourself unless asked.

The page must be served over HTTP — `file://` blocks `fetch`, and the renderer will
show an explanatory screen if so:

```
python3 -m http.server 8000 -d .visualize
# then open http://localhost:8000
```

**The served file must be named `index.html`, and the server's root must be the
`.visualize` folder itself.** Both are easy to get wrong:

- Serving the skill/project root instead of `.visualize` gives `404` on `/`, because
  there is no `index.html` at that level.
- Some static servers (including `serve`) rewrite `/visualizer.html` → `/visualizer`
  via "clean URLs", producing a confusing `301` then `404`. Naming the file
  `index.html` and serving `/` sidesteps this entirely.
- Relative `fetch('./root.json')` resolves against the URL directory, so the JSON must
  sit next to the HTML in the served root.

## What the renderer does with your data

Useful to know so you can shape JSON well. You do not need to read the file to trust this.

**Layout is a factory city, not a ladder or spreadsheet.** Each `group` becomes an
irregular **district/building** containing varied, slightly offset module machines.
Districts are packed into a roughly 16:10 footprint, largest first. Thick stepped
roads occupy the gaps between them: dark pavement, heavy shoulders, service dots,
direction marks, bends, and junction plates. Active dependency cables run over this
road network. This matters for how you shape the data:

- **Cluster count drives readability far more than node count.** 6–12 clusters reads
  as a city. 19+ tiny clusters reads as confetti — merge aggressively.
- **Avoid one giant catch-all cluster.** If `shared` or `utils` holds 40% of nodes,
  split it by purpose; one district will otherwise dominate the map.
- Aim for roughly comparable district sizes. Wildly uneven clusters pack poorly.
- `kind: "entry"` draws an amber edge stripe; `kind: "hot"` gets an accent outline.
- `weight` (fan-in) decides label priority, so get it right on the important files.

**Nothing is active at rest.** No wires render until the user selects something. The
status bar shows real counts instead of an instructional overlay.

**Interaction.** Click a chip to reveal relations; click again or click empty floor to
rest. Shift/cmd-click pins extra modules (union). Relation switches: `depends on` /
`used by` / `both` (`1` `2` `3`), kind toggles for imports/writes, depth 1–3 (`[` `]`).
`trace path` runs BFS between 2+ pins. Hover ghost-previews immediate links. Left index
lists modules grouped by cluster; right inspector has depends-on / used-by / reach tabs
where every row re-focuses that module. Arrow keys walk neighbours. Minimap
click-to-jump. `/` search, `esc` reset, `f` fit.

**Search is indexed once and relation-aware.** Plain terms use ranked fuzzy matching on
file names and paths; relation names are included conservatively. Multiple terms are
ANDed, quoted phrases are supported, and prefixing any term with `-` excludes it.
Structured filters:

- `.ts` or `type:ts` / `ext:ts` — exact extension (`.ts` does not include `.tsx`)
- `group:auth` — domain/cluster
- `kind:hot` — node kind (`entry`, `core`, `hot`)
- `uses:modal` or `depends:modal` — modules that depend on a matching module
- `usedby:route` or `caller:route` — modules used by a matching module
- `rel:queue` — either relation direction

Filters compose, e.g. `type:ts group:auth -kind:entry`. Enter opens the highest-ranked
result. When a result is selected, its actual relation traversal takes precedence over
the search filter so dependencies remain visible.

**Labels** live in a screen-space layer that is never scaled, so text stays crisp at any
zoom. A priority + collision pass places each one right → left → above → below, drawing
a leader line when offset, and **drops** rather than stacks labels that do not fit. A
zoom-dependent budget keeps wide views readable.

If a graph renders too sparse or too cluttered, adjust the **data** — fewer nodes,
tighter clusters, pruned edges — not the renderer.

