code-visualizer

v2026.09.24

Auto-generates code flow diagrams from multi-language module analysis. Detects when architecture diagrams become stale (code changed, diagram didn't). Supports Python, TypeScript/JavaScript, Rust, and Go out of the box. Use when: creating new modules, reviewing PRs for architecture impact, or checking diagram freshness across polyglot repositories. Generates mermaid diagrams showing imports, dependencies, and module relationships.

GitHub
Install command
npx skhub add rysweet/code-visualizer
Markdown
SKILL.md

Code Visualizer Skill

Purpose

Automatically generate and maintain visual code flow diagrams across multiple programming languages. The skill auto-detects which languages are present in a target path, analyzes each one with a dedicated analyzer, and emits one mermaid diagram per language plus an optional combined high-level view. It also detects when committed diagrams are stale relative to the source they describe.

What's New in 2.0.0

  • Multi-language support: Python, TypeScript/JavaScript, Rust, and Go.
  • Language dispatcher: Detects languages by file extension and routes to per-language analyzers.
  • Language-blind renderer: A single mermaid renderer consumes a normalized graph; the renderer never inspects language semantics.
  • One diagram per language plus an optional --combined view that places each language in its own mermaid subgraph.
  • Generalized staleness: Walks all source files matching detected languages' extensions and compares max-mtime against the diagram mtime.
  • Brick-style architecture: Each language analyzer is a self-contained module that exposes a single normalize() function. No shared inheritance.

Supported Languages

LanguageExtensionsAnalyzerParserNotes
Python.pypython_analyzerastExtracts import and from … import ….
TypeScript/JavaScript.ts, .tsx, .js, .jsx, .mjs, .cjsts_analyzerregexExtracts import … from, require(...), dynamic import(...).
Rust.rsrust_analyzerregexExtracts use crate::…, use super::…, mod ….
Go.gogo_analyzerregexExtracts single and grouped import declarations.

Languages outside this table are skipped silently. See Extending below to add new ones.

Architecture

amplifier-bundle/skills/code-visualizer/
├── SKILL.md
├── README.md
└── scripts/
    ├── __init__.py
    ├── graph.py              # Normalized data contract (Node, Edge, Graph)
    ├── python_analyzer.py    # normalize(paths) -> Graph
    ├── ts_analyzer.py        # normalize(paths) -> Graph
    ├── rust_analyzer.py      # normalize(paths) -> Graph
    ├── go_analyzer.py        # normalize(paths) -> Graph
    ├── dispatcher.py         # detect languages, route, return dict[lang, Graph]
    ├── mermaid_renderer.py   # render(graph) / render_combined(graphs)
    ├── staleness.py          # is_stale(target, diagram, languages)
    └── visualizer.py         # CLI entry point

Data Contract (graph.py)

@dataclass(frozen=True)
class Node:
    id: str           # mermaid-safe identifier
    label: str        # human-readable label (e.g. "src/auth/oauth.py")
    language: str     # "python" | "typescript" | "rust" | "go"
    file_path: str    # absolute path on disk

@dataclass(frozen=True)
class Edge:
    src: str          # Node.id of source
    dst: str          # Node.id of destination
    kind: str         # "import" | "require" | "use" | "mod" | "dynamic_import"

@dataclass(frozen=True)
class Graph:
    language: str
    nodes: tuple[Node, ...]
    edges: tuple[Edge, ...]

Analyzers may import these dataclasses but must not inherit from any shared class. The data contract is the only coupling.

Per-Language Analyzers

Each analyzer is a self-contained brick exposing exactly one entry point:

def normalize(paths: Iterable[Path]) -> Graph: ...

The function:

  1. Reads each file with encoding="utf-8", errors="ignore".
  2. Skips files larger than ~5 MB.
  3. Wraps parsing in try/except and skips files that fail to parse.
  4. Returns a Graph whose language field matches the analyzer.

Dispatcher

The dispatcher uses a registry that maps language name → extensions + module name (string). It loads analyzers lazily via importlib.import_module so adding a new language never requires touching the dispatcher's import statements.

from scripts.dispatcher import analyze

graphs: dict[str, Graph] = analyze(target_path)
# {"python": Graph(...), "typescript": Graph(...)}

The dispatcher:

  • Walks target_path with os.walk(..., followlinks=False).
  • Skips IGNORE_DIRS (.git, node_modules, .venv, venv, __pycache__, dist, build, target, .mypy_cache, .pytest_cache, .tox).
  • Buckets files by extension into language groups.
  • Calls each language's normalize() with its file list.
  • Returns a dict[language_name, Graph] for languages that produced any files.

Mermaid Renderer

The renderer is language-blind:

from scripts.mermaid_renderer import render, render_combined

per_language: str = render(graph)            # one diagram for one language
combined: str = render_combined(graphs)      # one diagram, one subgraph/lang

Node IDs are sanitized ([^A-Za-z0-9_] -> _) and labels with quotes are escaped to prevent diagram-syntax injection.

Staleness Detection

from scripts.staleness import is_stale

stale = is_stale(
    target_path=Path("src/"),
    diagram_path=Path("docs/architecture-python.mmd"),
    languages=["python"],
)

Returns True if any source file with a matching language extension has an mtime newer than diagram_path. Generalizes the previous Python-only behavior.

CLI

The skill ships a single executable: scripts/visualizer.py.

python visualizer.py <path> [--output DIR] [--basename NAME]
                            [--check-staleness] [--combined]
FlagDefaultPurpose
<path>requiredDirectory to analyze. Must exist and be a directory.
--output DIR./diagramsOutput directory for .mmd files.
--basename NAMEarchitectureFilename stem. Validated against ^[A-Za-z0-9._-]+$.
--check-stalenessoffPrint staleness report for existing diagrams; exit non-zero if stale.
--combinedoffAlso write <basename>-combined.mmd containing all languages.

Output Files

FileContents
<basename>-python.mmdMermaid diagram for Python modules and their imports.
<basename>-typescript.mmdMermaid diagram for TS/JS files and their imports.
<basename>-rust.mmdMermaid diagram for Rust modules and use edges.
<basename>-go.mmdMermaid diagram for Go packages and import edges.
<basename>-combined.mmd (with --combined)One diagram with one subgraph per detected language.

Files are only written for languages that were actually detected.

Quick Start

Generate diagrams for a polyglot repo

python amplifier-bundle/skills/code-visualizer/scripts/visualizer.py . \
    --output docs/diagrams --combined

Output (for this repo, which contains Python and JS):

docs/diagrams/architecture-python.mmd
docs/diagrams/architecture-typescript.mmd
docs/diagrams/architecture-combined.mmd

Check freshness in CI

python amplifier-bundle/skills/code-visualizer/scripts/visualizer.py src/ \
    --output docs/diagrams --check-staleness
# exits 1 if any per-language diagram is older than its source set

Generate for a single language

Provide a path that only contains files of one language; the dispatcher will detect a single language and emit a single .mmd:

python visualizer.py src/auth/      # Python-only -> architecture-python.mmd

Auto-Detection Rules

  1. The dispatcher walks <path>, skipping IGNORE_DIRS and symlinks.
  2. Files are bucketed by extension into one of the supported languages.
  3. A language is "detected" if at least one file matches.
  4. Each detected language is analyzed independently.
  5. With --combined, the renderer composes one mermaid diagram with one subgraph per detected language. Cross-language edges are not inferred in the MVP.

Example Output

For a repo with:

  • src/api.py importing src/auth.py
  • web/index.ts importing web/utils.ts

architecture-python.mmd:

flowchart TD
    src_api_py["src/api.py"]
    src_auth_py["src/auth.py"]
    src_api_py --> src_auth_py

architecture-typescript.mmd:

flowchart TD
    web_index_ts["web/index.ts"]
    web_utils_ts["web/utils.ts"]
    web_index_ts --> web_utils_ts

architecture-combined.mmd:

flowchart TD
    subgraph python ["python"]
        src_api_py["src/api.py"]
        src_auth_py["src/auth.py"]
        src_api_py --> src_auth_py
    end
    subgraph typescript ["typescript"]
        web_index_ts["web/index.ts"]
        web_utils_ts["web/utils.ts"]
        web_index_ts --> web_utils_ts
    end

Note: the renderer emits the subgraph <id> ["<label>"] form (space between id and bracketed label), which is the Mermaid-documented syntax accepted across recent Mermaid versions. test_mermaid_renderer.py pins the exact emitted form.

Extending: Adding a New Language

The skill follows the brick philosophy: a new language is a new self-contained module. There is no base class to subclass.

  1. Create scripts/<lang>_analyzer.py with the entry point:

    from collections.abc import Iterable
    from pathlib import Path
    from graph import Edge, Graph, Node  # sibling import; works under `python visualizer.py`
    
    def normalize(paths: Iterable[Path]) -> Graph:
        nodes: list[Node] = []
        edges: list[Edge] = []
        for p in paths:
            # parse file, append nodes/edges
            ...
        return Graph(language="<lang>", nodes=tuple(nodes), edges=tuple(edges))
    
  2. Register the language in scripts/dispatcher.py:

    LANGUAGES = {
        "python":     {"exts": {".py"},                          "module": "python_analyzer"},
        "typescript": {"exts": {".ts", ".tsx", ".js", ".jsx",
                                ".mjs", ".cjs"},                 "module": "ts_analyzer"},
        "rust":       {"exts": {".rs"},                          "module": "rust_analyzer"},
        "go":         {"exts": {".go"},                          "module": "go_analyzer"},
        # add here:
        "<lang>":     {"exts": {".ext"},                         "module": "<lang>_analyzer"},
    }
    
  3. Add tests/test_<lang>_analyzer.py with tmp_path fixtures asserting nodes and edges produced by representative source snippets.

  4. Update the Supported Languages table above.

That's it. The renderer, dispatcher routing, staleness detector, and CLI all work without further changes because they consume the language-blind Graph data contract.

Testing

Tests live under amplifier-bundle/skills/code-visualizer/tests/ and run via pytest. The skill registers its tests/ directory in the repo's pytest.ini testpaths so CI picks them up automatically.

Test files:

FilePurpose
test_python_analyzer.pyAST-driven import extraction; verifies edges for import/from.
test_ts_analyzer.pyimport/require/dynamic import(); type-only and relative paths.
test_dispatcher.pyMixed-language fixture; verifies correct routing per extension.
test_mermaid_renderer.pyEmpty graphs, non-empty graphs, ID/label sanitization.
test_staleness.pyMtime comparison across multiple language extensions.
test_smoke_repo.pyRuns dispatcher against the repo root; asserts non-empty mermaid
for both Python and TypeScript/JavaScript.

Run only the skill's tests:

pytest amplifier-bundle/skills/code-visualizer/tests -q

Security Considerations

  • No code execution: Analyzers only parse source. No exec/eval/ subprocess on analyzed files.
  • Path validation: <path> and --output are resolved with Path.resolve() and rejected if non-existent or non-directory.
  • Filename validation: --basename must match ^[A-Za-z0-9._-]+$.
  • Symlink safety: os.walk(..., followlinks=False) plus IGNORE_DIRS prevents loops and escape.
  • Bounded reads: Per-file size cap (~5 MB); UTF-8 decode with errors="ignore".
  • Bounded regex: Anchored, no nested quantifiers; protects against ReDoS.
  • Mermaid sanitization: Node IDs strip non-[A-Za-z0-9_]; labels with embedded quotes are escaped.
  • Stdlib-only: Zero third-party runtime dependencies; no supply-chain surface.
  • Output containment: Writes are constrained to the resolved --output directory; source content is never logged.

Limitations

  • Static heuristics: Regex-based extraction for TS/JS/Rust/Go misses some edge syntax (TS type-only imports across multiple lines, Rust nested use {a, b::c}, Go cgo blocks). Documented per analyzer in source.
  • No call graphs: Edges are import/use only. Runtime/dynamic imports beyond import("...")/__import__ are not modeled.
  • External imports: Rendered as ghost target nodes inline; not resolved to real files.
  • Combined view: Cross-language edges are out of MVP scope.
  • Shell scripts: Not first-class; .sh files are ignored.
  • Compiler-grade accuracy: Not a goal. The skill optimizes for "useful diagram in seconds" over "perfect AST."

Philosophy Alignment

PrincipleHow v2.0 follows it
Ruthless SimplicityStdlib-only; regex over tree-sitter; max-mtime over semantic diff.
Zero-BSReal parsers (ast for Python, regex for others). Limitations documented honestly.
Modular DesignEach analyzer is a brick with a single normalize() stud. No inheritance.
Brick CompositionRenderer/dispatcher/staleness are independent bricks reusing only the data contract.

Migration from 1.x

The 1.x skill was Python-only. Forward-compatibility notes (verify against your actual 1.x integration before relying on them):

  1. Diagrams previously named <basename>.mmd are now <basename>-python.mmd. Update any references in README.md / ARCHITECTURE.md.
  2. Staleness reports now include a per-language breakdown. CI scripts that parsed the old single-line output should be updated to handle multiple languages.
  3. Any direct Python helper used in 1.x is superseded by dispatcher.analyze(path) returning a dict[language, Graph]. Callers that only want Python can use dispatcher.analyze(path)["python"].

Remember

The skill automates what developers forget across all four supported languages: keeping diagrams in sync with code. It's not a compiler; it's a fast, honest, multi-language snapshot.

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

Not specified

Source path

.claude/skills/code-visualizer

Default branch

main

Latest commit

8442445

Tree SHA

60d91dd