Skip to main content

Structural call-graph diff for codebases, built for coding agents

Project description

codiff

PyPI GitHub Marketplace

A structural call-graph diff tool for multi-language codebases, built to work with coding agents.

Instead of a line diff, codiff shows what changed at the function level: which functions were added and where they hook into existing code, which were modified, which were removed — with call relationships mapped across files.

No LLM, no embeddings, fully offline.

Supported languages

Language Extensions
Python .py
TypeScript .ts, .tsx

Installation

pip install codiff

Quickstart

# Run from inside any git repository
codiff diff

This compares your working tree against HEAD. If everything is already committed, use --base and --head to compare two refs explicitly:

# Diff between two branches
codiff diff --base main --head my-feature

# Diff introduced by the last commit only
codiff diff --base HEAD~1 --head HEAD

# Diff between two arbitrary commits
codiff diff --base a1b2c3d --head e4f5g6h

To get a Mermaid diagram (useful for PR descriptions):

codiff diff --base main --format mermaid

To save the diagram to a file and preview it:

codiff diff --base main --format mermaid > mermaid.md

Open mermaid.md in any Markdown previewer (VS Code, GitHub, etc.) to visualise the diagram.

GitHub Action

codiff is available as a GitHub Action that posts a structural diff comment on every pull request — no AI coding agent required. The comment is created when the PR opens and updated on every subsequent push.

Run once in your repository to set it up:

codiff init --agent github-actions

This creates .github/workflows/codiff.yml:

name: codiff

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  codiff:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - uses: issahammoud/codiff-action@v1

Commit and push the file to activate the action.

Note: Use either the GitHub Action or an AI coding agent integration, not both. If both are active, codiff detects the redundancy automatically and the action stays silent when the agent has already embedded a diagram in the PR description.

Agent integration (MCP)

Run once per project to wire codiff into your coding agent:

codiff init --agent claude      # Claude Code
codiff init --agent codex       # OpenAI Codex CLI
codiff init --agent gemini      # Gemini CLI
codiff init --agent vibe        # Mistral Vibe

This writes the MCP server config and a project instructions file that tells the agent to call codiff_diff when creating pull requests. The agent then automatically embeds a structural diagram in every PR description.

Agent MCP config Instructions file
claude .mcp.json CLAUDE.md
codex .codex/config.toml AGENTS.md
gemini ~/.gemini/settings.json GEMINI.md
vibe ~/.vibe/config.toml

Here's the output of codiff diff --base b8dde67 --head d25c385 --format mermaid run on codiff's own codebase:

%%{init: {'layout': 'elk', 'elk': {'direction': 'RIGHT'}, 'maxTextSize': 999999, 'theme': 'base', 'themeVariables': {'background': '#ffffff', 'clusterBkg': '#f8fafc', 'clusterBorder': '#94a3b8', 'primaryColor': '#f8fafc', 'primaryBorderColor': '#94a3b8', 'primaryTextColor': '#1e293b', 'lineColor': '#64748b', 'fontSize': '13px', 'fontFamily': 'ui-monospace, SFMono-Regular, Menlo, monospace'}}}%%
classDiagram
    direction LR

    class n5["codiff/cli.py"] {
        ~ _run_diff()  sig
        ~ main()
    }

    class n7["codiff/diff/indexer.py"] {
        + _incremental_update_db()
        ~ _full_index()  sig
        ~ ensure_indexed()  sig
    }

    class n3["PythonParser"] {
        <<codiff/languages/python/parser.py>>
        ~ build_package_exports()  calls +1−1
    }

    class n8["codiff/diff/snapshot.py"] {
        + _chunk_to_node_info()
        + _git_changed_files()
        + _parse_and_expand_stale()
        + build_snapshot_incremental()
        ~ build_from_path()  sig
        ~ build_from_ref()  sig
        ~ load_from_db()  calls +1−1
    }
    class n0["_ClassStub"] {
        <<codiff/diff/snapshot.py>>
        + __init__()
    }
    class n1["_NodeStub"] {
        <<codiff/diff/snapshot.py>>
        + __init__()
    }

    class n2["LanguageParser"] {
        <<codiff/languages/parser.py>>
        ~ build_modules_dict()  calls +1−1
    }

    class n6["codiff/db/operations.py"] {
        + _insert_call_edges()
        + _insert_classes()
        + _insert_functions()
        + _session()
        + get_indexed_sha()
        + load_snapshot()
        + update_sha()
        + write_full_snapshot()
        + write_incremental()
    }

    style n5 fill:#fefce8,color:#854d0e,stroke:#f59e0b,stroke-width:3px
    style n7 fill:#fefce8,color:#854d0e,stroke:#f59e0b,stroke-width:3px
    style n3 fill:#fefce8,color:#854d0e,stroke:#f59e0b,stroke-width:3px
    style n8 fill:#fefce8,color:#854d0e,stroke:#f59e0b,stroke-width:3px
    style n0 fill:#f0fdf4,color:#166534,stroke:#22c55e,stroke-width:3px
    style n1 fill:#f0fdf4,color:#166534,stroke:#22c55e,stroke-width:3px
    style n2 fill:#fefce8,color:#854d0e,stroke:#f59e0b,stroke-width:3px
    style n6 fill:#f0fdf4,color:#166534,stroke:#22c55e,stroke-width:3px

    %% Relationships
    n3 --|> n2
    n5 --> n8 : calls
    n7 --> n0 : calls
    n7 --> n1 : calls
    n7 --> n6 : calls
    n7 --> n8 : calls
    n8 --> n0 : calls
    n8 --> n1 : calls
    n8 --> n6 : calls

Each box is a file or class. Green = only additions, yellow = at least one modification. Inside each box: + added function, ~ modified. Annotations: sig = signature changed, calls +N−N = now calls N more / N fewer functions. Arrows show which files call into which.

How it works

codiff maintains a SQLite call-graph index (.codiff.db) at the repo root. On first run it does a full parse; on subsequent runs it re-parses only the files changed since the last indexed commit, then detects stale callers (functions whose callees were renamed or deleted) and re-parses those too. Both the base and head snapshots are built incrementally from this index, so diffs stay fast even on large codebases.

The graph delta — added, modified, removed functions — is computed deterministically from the resolved call graph.

Requirements

  • Python 3.11+
  • Git

CLI reference

# Index a repository (writes .codiff.db — runs automatically on first diff)
codiff index <path>

# Diff
codiff diff                          # diff HEAD vs working tree (terminal output)
codiff diff --format mermaid         # output a Mermaid class diagram
codiff diff --format json            # output structured JSON (for editor integrations)
codiff diff --base main              # diff a specific base ref
codiff diff --head <ref>             # diff two git refs directly
codiff diff --repo /path/to/repo     # diff a different repo
codiff diff --include-tests          # include test functions (hidden by default)
codiff diff --include-deleted        # include deleted functions (hidden by default)
codiff diff --workers 8              # set parallel worker count (default: cpu_count // 2)
codiff diff --debug                  # print timing breakdown for each processing step

# Configure a coding agent
codiff init --agent <agent>

Output formats

Format Description
terminal Colored terminal output with UML-style boxes (default)
mermaid Two Mermaid classDiagram blocks — paste into any Markdown file or PR description
json Structured JSON — consumed by editor integrations (e.g. the VS Code extension)

Reading the terminal output

The output shows one box per changed file. Boxes are laid out side by side when they fit the terminal width, with labeled arrows between adjacent connected boxes.

Inside each file box

Methods belonging to the same class are grouped into a dashed sub-box (╭╌╌╌ ClassName ╌╌╌╮). Standalone functions appear directly in the file box. Deleted functions (only shown with --include-deleted) are collected into a red ╭╌╌╌ deleted ╌╌╌╮ sub-box.

Functions are listed with an indicator and an annotation:

Indicator Meaning
+ green Function was added
~ yellow Function was modified
- red Function was removed
Annotation Meaning
entry point Nothing calls this new function — new public surface
sig changed Parameters or return type changed
calls changed The function now calls different things
body changed Pure implementation change

For added functions, arrows show intra-file call relationships — a function indented under another calls it.

Intra-file class relationships

When two changed classes in the same file are related, each class box shows a dim annotation before its methods:

╭╌╌╌ PageAwarePreChunker ╌╌╌╮
│ calls  PageAwareBuilder   │
│ ──────────────            │
│ + __init__                │
╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯

Relationship types: calls (method calls to another class) and inherits (superclass relationship detected from class definitions).

Colors

Functions that form a connected call chain share a color across the entire output — across boxes, across files. All magenta names belong to one chain, all cyan to another.

  • Chain color on the function name — part of a call chain
  • White on the function name — added/modified but not connected to any chain
  • ~ yellow — always marks a modified function regardless of chain membership

Arrows between file boxes

Labeled arrows appear between adjacent file boxes when there is a cross-file relationship:

Label Meaning
calls ────▶ A function in the left file calls a function in the right file
inherits ────▶ A class in the left file inherits from a class in the right file

Reading the Mermaid output

The Mermaid format produces two diagrams:

  1. Connected modules — files that have call or inheritance relationships with other changed files. Rendered with ELK layout left-to-right, with calls and inherits arrows between class boxes.

  2. Isolated modules — files with no cross-file relationships (e.g. migration files, config). Grouped by their top two folder levels into namespace clusters, rendered with Dagre left-to-right.

Both diagrams use color-coded class boxes:

  • Green — only additions in this file/class
  • Yellow — at least one modification
  • Red — only deletions (requires --include-deleted)

Inside each box, functions are listed with a prefix and optional annotation:

Prefix Meaning
+ Function was added
~ Function was modified
- Function was removed
Annotation Meaning
sig Parameters or return type changed
calls +N−N Call list changed — N new callees, N dropped

When a class appears inside a file box, its file path is shown as a «stereotype» subtitle below the class name.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

codiff-0.1.3.tar.gz (126.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

codiff-0.1.3-py3-none-any.whl (83.3 kB view details)

Uploaded Python 3

File details

Details for the file codiff-0.1.3.tar.gz.

File metadata

  • Download URL: codiff-0.1.3.tar.gz
  • Upload date:
  • Size: 126.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for codiff-0.1.3.tar.gz
Algorithm Hash digest
SHA256 8e7532a5039024ef8faed0d691dc67d164d68550a7a944bf7720040e80140ff1
MD5 3f536ba4dc66ecadc41db5d6cad7c8f7
BLAKE2b-256 55f194f9281c8e3be6cdc5334bb3720e76e535a134f25c3fd742861f2295bec6

See more details on using hashes here.

Provenance

The following attestation bundles were made for codiff-0.1.3.tar.gz:

Publisher: publish.yml on issahammoud/codiff

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file codiff-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: codiff-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 83.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for codiff-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 a311d229a4764a3a6d7523ee8427c4640f557b9a946d59f1ae6ed2b241191184
MD5 b709564c424c6bc28b3d7836c7c8c270
BLAKE2b-256 0e35d55bd0fcdd266c8b0753fe9f82f4d65da1aadd7eeb015a053272909cd874

See more details on using hashes here.

Provenance

The following attestation bundles were made for codiff-0.1.3-py3-none-any.whl:

Publisher: publish.yml on issahammoud/codiff

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page