depdog

module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT

README

depdog

A Codebase Dependency Watchdog — your architecture rules, enforced on every build.

Latest release CI

Install · Quick start · Configuration  · CI · Commands · Docs

Architecture rules usually live in someone's head or a wiki, and they rot. depdog makes rules executable.

No more import spaghetti.

You declare who may import whom in a depdog.yaml, and depdog checks it against every import edge in your codebase, exiting non-zero for CI. One tool, one engine, polyglot across languages through thin, hot-swappable adapters. Mixed monorepos supported.

depdog check — github.com/matterpale/depdog

✗ core: allow [std]  (2 violations)
    github.com/matterpale/depdog/internal/core
      → github.com/matterpale/depdog/internal/report   internal/core/evaluate.go:9
      → github.com/charmbracelet/lipgloss              internal/core/core.go:12

2 violations · 10 packages · 107 edges checked in 112ms

That's depdog checking its own repo: its architecture is declared in depdog.yaml and enforced in CI — a failing architecture is a failing build.

Use cases

CI

depdog check exits non-zero on any violation and speaks github and sarif.

Coding agents

A stable JSON schema, contract exit codes, and a skill help your agent get you started.

Local exploration

The TUI and depdog explain help with reading an existing graph and debugging by hand.

LSP for your IDE

depdog lsp surfaces violations as inline diagnostics in the editor of your choice.

Install

Homebrew (macOS):

brew install --cask matterpale/tap/depdog

Go:

go install github.com/matterpale/depdog/cmd/depdog@latest

Prebuilt binaries for Linux, macOS, and Windows are on the releases page; building from source (go build -o depdog ./cmd/depdog) needs Go 1.26+.

Quick start

depdog init      # interactively kick off a starter depdog.yaml
depdog check     # check against the rules; exit 1 on violations

init inspects your layout, matches it against an architecture preset, and proposes a component mapping you refine interactively — drop, rename, or re-path components. Or accept all as-is with --yes.

For a tight local loop, depdog check --watch re-runs the check whenever a file changes (text output; Ctrl-C to stop) — the terminal-first counterpart to the editor LSP, catching a broken boundary as you edit rather than in CI.

[!TIP] Alternatively, ask a coding agent to get you started with the dedicated skill.

Configuration

depdog.yaml lives at the repo root, next to go.mod:

version: 2

# Each component lists its path glob(s) and, inline, who it may import.
components:
  main: { path: "cmd/**" }                                # no rule → open (the default)
  domain: { path: "internal/domain/**", allow: [ std ] }  # whitelist: std only
  handler: { path: "internal/handler/**" }
  service: { path: "internal/service/**" }
  repository: { path: "internal/repository/**" }

# A boundary isolates its members from each other: one line, no O(n²) deny lists.
boundaries:
  layers: [ handler, service, repository ]

default: allow   # fallback for a rule-less component (like main); the default if omitted

options:
  test_files: hybrid                # default; also: same-rules, relaxed
  skip: [ "internal/legacy/**" ]    # package dirs excluded from analysis

Here domain is an allow list — only what's listed passes. The layers boundary keeps the three peers out of each other — the same effect as giving each a deny list naming its two siblings, in one line. deny lists still exist for one-off exclusions.

Graduated severity. A component or boundary can carry severity: warn (default error). Its violations are then reported on every surface but do not fail the build — only error violations flip the exit code. Pair it with the baseline ratchet to warn on the messy edges while failing on new ones:

components:
  legacy: { path: "internal/legacy/**", allow: [ std ], severity: warn }

An editor JSON Schema ships at schema/depdog.schema.json for autocomplete and validation (a test keeps it in lockstep with the parser).

Full reference — docs/configuration.md: component matching and precedence, the complete allow/deny vocabulary, aliases, the non-blocking signals (unmapped packages, dead patterns, component cycles), and test-file handling. Boundaries have their own page — docs/boundaries.md.

CI

depdog check is CI-ready as-is. For inline pull-request annotations use the github format; for code scanners, use sarif:

- run: go run github.com/matterpale/depdog/cmd/depdog check --format github

# or, for the code-scanning tab:
- run: go run github.com/matterpale/depdog/cmd/depdog check --format sarif > depdog.sarif
- uses: github/codeql-action/upload-sarif@v3
  with: { sarif_file: depdog.sarif }

Or use the composite action — it downloads the released binary (no Go toolchain needed on the runner, so it works for any-language repos) and runs depdog:

- uses: matterpale/depdog@v0.6.0
  with:
    version: latest                    # a tag like v0.6.0, or "latest"
    args: check --all --format github

For a polyglot monorepo, one step governs every language at once — from the repo root, --all discovers every depdog.yaml under the tree, checks each unit against its own auto-detected adapter, and aggregates into one report with a single exit code:

- run: go run github.com/matterpale/depdog/cmd/depdog check --all --format github

See monorepos for what a unit is and how discovery works.

Ratchet-friendly

For a codebase that doesn't pass yet: record today's violations as a baseline, then fail only on new ones — and shrink the baseline over time:

depdog baseline                 # writes depdog.baseline.yaml
depdog check --fail-on new      # exits 1 only on violations not in the baseline

Pre-commit hook

Catch a broken architecture before it reaches CI. Install a git hook directly:

depdog install-hook   # writes .git/hooks/pre-commit → runs `depdog check`

It is idempotent and won't overwrite a pre-commit hook it didn't write (pass --force to replace one). Or, with the pre-commit framework, add to .pre-commit-config.yaml:

repos:
  - repo: https://github.com/matterpale/depdog
    rev: v0.6.0
    hooks:
      - id: depdog

Diff your architecture per PR

depdog diff --since <ref> reports how a change moved the architecture: the cross-component import edges a branch adds and removes relative to a git ref, each flagged when it crosses a boundary — e.g. "3 new cross-component edges (1 crosses the adapters boundary), 1 removed". It is informational (always exits 0, unlike check), so it never blocks a merge — it surfaces new structure a reviewer should notice.

Post it as a PR comment with the GitHub-flavoured markdown format:

- name: Architecture diff
  run: |
    go run github.com/matterpale/depdog/cmd/depdog diff \
      --since origin/${{ github.base_ref }} --format github > diff.md
    gh pr comment ${{ github.event.number }} --body-file diff.md
  env: { GH_TOKEN: ${{ github.token }} }

Use --format json for tooling — a stable, sorted { since, added[], removed[], stats } delta (snake_case; empty collections are [], never null):

depdog diff --since origin/main --format json | jq '.stats'

The "before" graph is the ref materialized in a throwaway git worktree; both graphs are mapped to components under the current depdog.yaml, so the diff reflects structural movement, not a config change.

Trend your architecture over time

Where diff shows a single PR's movement, depdog trend --since <ref> samples commits from a ref to HEAD and plots how the health metrics moved — so you catch architecture drifting before it becomes violations. Each sampled commit is scanned in a throwaway worktree under the current depdog.yaml, so the trend is about the code, not config churn.

depdog trend --since v1.0.0        # a table + first→last delta; --max caps the samples
depdog trend --since v1.0.0 --format json | jq '.delta'

Commands

Command What it does
depdog init Scan the module and write a starter depdog.yaml; --merge extends an existing one in place
depdog check [packages] Evaluate every import edge against the rules
depdog graph Emit the dependency graph as DOT or Mermaid
depdog diff --since <ref> Show how a change moved the architecture vs a git ref: cross-component edges added/removed, boundary crossings flagged (informational, exits 0)
depdog metrics Report architecture-health numbers: per-component coupling (fan-in / fan-out) and instability, plus repo totals (edges, boundary crossings, cycles) — text or json (informational, exits 0)
depdog trend --since <ref> Trend those metrics over git history (samples up to --max commits from a ref to HEAD) so drift shows up before it becomes violations — text or json (informational, exits 0)
depdog explain <component-or-package> [import] Explain why something is red (rule/boundary that fired, with file:line), constraints, boundary membership etc.
depdog config Print the compiled rule set — components, patterns, inferred stances, boundaries, options — for debugging a config
depdog lsp LSP server over stdio: violations become inline editor diagnostics at their import lines (editor setup · design)
depdog mcp Read-only MCP server over stdio: check, explain and can_import tools plus config resources, for agents (docs/mcp.md)
depdog tui Interactive terminal UI: component dashboard, browsable violations, per-package imports and importers, a Config tab showing the compiled rules — and an experimental visual rule editor (keys)
depdog baseline Record current violations to depdog.baseline.yaml for the ratchet
depdog install-hook Install a git pre-commit hook that runs depdog check (idempotent; --force to replace a foreign hook)

Run bare, depdog evaluates the check — the same as depdog check, taking the same flags — so a plain depdog yields the real 0/1/2 exit code in a pipe or on a terminal. The interactive view is depdog tui.

Exit codes are a contract:

Code Meaning
0 clean
1 violations
2 error

Multi-language support

depdog checks many languages with the same depdog.yaml, the same commands, and the same engine.

Only a thin language adapter differs; the rule format is neutral — component path globs match project-relative directories, and std / external are abstract buckets each adapter fills. Every adapter is a pure-Go static import scanner — no language toolchain is required, depdog stays a single binary. (The one exception: the Go adapter resolves its package graph through go list metadata — see limitations.)

Language Detected by Scans
go Go go.mod package imports
rs Rust Cargo.toml use / mod / extern crate
py Python pyproject.toml, setup.py, setup.cfg import / from … import (incl. relative)
kt Kotlin build.gradle.kts, settings.gradle.kts package + import
java Java pom.xml, build.gradle package + import
scala Scala build.sbt, build.sc package + import (incl. {A,B}, ._, .*, given)
elm Elm elm.json module + import (module-name resolution)
rb Ruby Gemfile, .ruby-version, Rakefile require / require_relative / autoload
ts TS / JS tsconfig.json, package.json import/export from/require/dynamic import()

internal/core (the engine) never changed as languages were added — the whole point of the adapter registry is that a new language is one internal/lang/<x> package plus one registry entry.

depdog picks the adapter from the project's marker files automatically, and the persistent --lang flag forces a specific one — details, including nested layouts and two-language ambiguity, in docs/languages.md.

For AI agents

depdog is built to be driven by tools and agents, not just humans: check --format json emits a stable schema and the exit codes are a contract.

skills/depdog/SKILL.md is a self-contained, tool-agnostic playbook any coding agent can follow to work with depdog end to end — mapping a codebase to components, authoring the depdog.yaml, and validating with check. The editor JSON Schema hands the same autocomplete and validation to any schema-aware agent.

depdog also speaks MCPdepdog mcp exposes read-only check/explain/can_import tools (plus config/components resources) over stdio so an MCP-capable agent can consult the architecture in the loop. See docs/mcp.md.

LSP Setup

Wire depdog lsp into Neovim, Helix, VS Code (via the bundled editors/vscode extension scaffold), Zed, GoLand/JetBrains (via the LSP4IJ plugin), or Emacs for inline architecture diagnostics — per-editor snippets in docs/editors.md.

How depdog compares

Every major ecosystem has grown its own architecture linter — go-arch-lint for Go, dependency-cruiser for JS/TS, deptrac for PHP (Python has import-linter and Tach, Java has ArchUnit). Each is excellent inside its ecosystem, and each is welded to it — its language, and usually its toolchain. depdog's bet is different: one rule model, one engine, thin adapters.

depdog go-arch-lint dependency-cruiser deptrac
Languages many, one rule format Go JS/TS (+ Vue, Svelte) PHP
Needs one static binary* the Go toolchain Node + the project's own compilers PHP ≥ 8.2
Baseline ratchet
CI formats GitHub annotations · SARIF JSON Markdown · TeamCity · Azure DevOps GitHub annotations · JUnit · CodeClimate
Architecture diff per PR (edges ±) diff --since
Inline editor diagnostics (LSP)
Agent interface MCP · skill · semver-stable JSON JSON JSON · JS API JSON
Rules across languages in one monorepo depdog.work.yaml

*The Go adapter is the one exception: it resolves its package graph through go list metadata — see limitations.

vs go-arch-lint, the nearest neighbour on a Go codebase: go-arch-lint loads your packages through the Go toolchain — even its basic check shells out to go list, and its default-on deepscan fully type-checks the module, so your dependencies must resolve. The payoff is real — deepscan traces dependency injection through interfaces, catching inversions no import scan can see. depdog deliberately stays at the import layer: scan source text, never build — so it runs mid-refactor, on code that doesn't compile yet, and the identical engine covers many more languages. On top of that layer depdog adds what go-arch-lint doesn't have: the baseline ratchet, SARIF, an LSP, an MCP server, and per-PR architecture diffs.

Inside their home ecosystems, dependency-cruiser (regex rules, a dozen-plus output formats) and deptrac (semantic layer collectors: by attribute, interface, composer package) go deeper than depdog does. depdog's case is the column no per-language tool can fill: the same depdog.yaml in every corner of the repo, the agent surface, and cross-language rules between the units of a mixed monorepo.

Limitations

  • Static analysis. Every adapter scans source for import statements; it does not run or type-check your code. Fully dynamic imports (a computed require(someVar), a reflective Java classload) are invisible by design — architecture rules are about the imports you write.
  • Go adapter — one build configuration. The Go adapter loads packages for the host's GOOS/GOARCH and default build tags; imports guarded by other build constraints (e.g. //go:build windows on a non-Windows machine) aren't seen.
  • Go adapter — resolves via go list, but degrades gracefully. The Go adapter loads the package graph through go list for exact module / stdlib / replace classification. When go list can't resolve every import — a dependency isn't downloaded, or the code is mid-refactor — depdog does not abort: it falls back to a best-effort static scan (parsed imports + a path heuristic), prints a depdog: warning: to stderr that classification is approximate, and still checks the edges it can see. So depdog check runs on Go code with unresolved imports — a missing dependency, or a not-yet-created package mid-refactor — rather than aborting (a genuinely unparseable file still errors). --format json carries a "degraded": true flag so CI can tell an approximate run from an exact one; run go mod download for exact results.
  • Monorepos — per-unit fan-out; cross-unit rules are opt-in. depdog checks a monorepo by fanning out over its units, each checked independently against its own depdog.yaml, via two discovery kinds: go.work fan-out (automatic — inside a Go workspace depdog check fans out over every member module with a depdog.yaml; members without one are advisory-skipped, --module <path-or-dir> narrows, GOWORK=off opts out), and --all polyglot discovery (any language — from the repo root depdog check --all walks down, discovers every depdog.yaml, and checks each unit against its own auto-detected adapter; --unit <dir> narrows). Both aggregate into one report and a single exit code (max severity across units) in every --format. Within the fan-out each unit is checked in isolation, so an import from one unit into another classifies as external; governing the edges between units is the opt-in depdog.work.yaml layer on top. Note the cross-unit pass detects source-level references (resolved paths and import identities) — a plain HTTP call between services leaves no import to detect. Full guides: docs/monorepo.md, docs/cross-language.md.

License

MIT


🐕 woof.

Directories

Path Synopsis
cmd
depdog command
internal
cli
Package cli assembles depdog's command tree.
Package cli assembles depdog's command tree.
config
Package config loads depdog.yaml and compiles it into a core.RuleSet.
Package config loads depdog.yaml and compiles it into a core.RuleSet.
core
Package core holds depdog's language-agnostic domain model: the import graph produced by language adapters, the rule set compiled from the project config, and the evaluation that turns both into violations.
Package core holds depdog's language-agnostic domain model: the import graph produced by language adapters, the rule set compiled from the project config, and the evaluation that turns both into violations.
lang
Package lang defines the contract between depdog's core and language-specific analyzers.
Package lang defines the contract between depdog's core and language-specific analyzers.
lang/elm
Package elm is the Elm language adapter: it statically scans a project's .elm source files for their `module`/`port module`/`effect module` declaration and `import` statements, resolves each import against the modules the project itself ships under its elm.json source-directories, and builds the same directory-keyed *core.Graph the Go, TypeScript, Python, Rust, Java, Kotlin and Scala adapters produce.
Package elm is the Elm language adapter: it statically scans a project's .elm source files for their `module`/`port module`/`effect module` declaration and `import` statements, resolves each import against the modules the project itself ships under its elm.json source-directories, and builds the same directory-keyed *core.Graph the Go, TypeScript, Python, Rust, Java, Kotlin and Scala adapters produce.
lang/golang
Package golang is the Go language adapter: it loads a module's package graph via go/packages metadata (no type-checking) and resolves import positions with a lightweight parse of the import declarations.
Package golang is the Go language adapter: it loads a module's package graph via go/packages metadata (no type-checking) and resolves import positions with a lightweight parse of the import declarations.
lang/java
Package java is the Java language adapter: it statically scans a project's .java source files for their `package` declaration and `import` statements (import a.b.C;, import static a.b.C.m;, and on-demand import a.b.*;), resolves each import against the set of packages the project itself declares, and builds the same directory-keyed *core.Graph the Go, TypeScript, Python and Rust adapters produce.
Package java is the Java language adapter: it statically scans a project's .java source files for their `package` declaration and `import` statements (import a.b.C;, import static a.b.C.m;, and on-demand import a.b.*;), resolves each import against the set of packages the project itself declares, and builds the same directory-keyed *core.Graph the Go, TypeScript, Python and Rust adapters produce.
lang/kotlin
Package kotlin is the Kotlin language adapter: it statically scans a project's .kt (and .kts) source files for their `package` declaration and `import` statements (import a.b.C, import a.b.*, and import a.b.C as D), resolves each import against the set of packages the project itself declares, and builds the same directory-keyed *core.Graph the Go, TypeScript, Python, Rust and Java adapters produce.
Package kotlin is the Kotlin language adapter: it statically scans a project's .kt (and .kts) source files for their `package` declaration and `import` statements (import a.b.C, import a.b.*, and import a.b.C as D), resolves each import against the set of packages the project itself declares, and builds the same directory-keyed *core.Graph the Go, TypeScript, Python, Rust and Java adapters produce.
lang/python
Package python is the Python language adapter: it statically scans a project's .py/.pyi source files for import statements (import a.b, import a.b as c, from a.b import x, and relative from .
Package python is the Python language adapter: it statically scans a project's .py/.pyi source files for import statements (import a.b, import a.b as c, from a.b import x, and relative from .
lang/ruby
Package ruby is the Ruby language adapter: it statically scans a project's .rb source files for require statements (require "x", require_relative "../x", and autoload :C, "x"), resolves them against the on-disk file layout, and builds the same directory-keyed *core.Graph the Go and TypeScript adapters produce.
Package ruby is the Ruby language adapter: it statically scans a project's .rb source files for require statements (require "x", require_relative "../x", and autoload :C, "x"), resolves them against the on-disk file layout, and builds the same directory-keyed *core.Graph the Go and TypeScript adapters produce.
lang/rust
Package rust is the Rust language adapter: it statically scans a crate's .rs source files for the import surfaces Rust exposes (use crate::a::b;, use self::/super::.., use ext_crate::.., grouped use a::{b, c};, pub use .., mod name;, and extern crate x;), resolves them against the on-disk module layout under the crate's src tree, and builds the same directory-keyed *core.Graph the Go, TypeScript and Python adapters produce.
Package rust is the Rust language adapter: it statically scans a crate's .rs source files for the import surfaces Rust exposes (use crate::a::b;, use self::/super::.., use ext_crate::.., grouped use a::{b, c};, pub use .., mod name;, and extern crate x;), resolves them against the on-disk module layout under the crate's src tree, and builds the same directory-keyed *core.Graph the Go, TypeScript and Python adapters produce.
lang/scala
Package scala is the Scala language adapter: it statically scans a project's .scala source files for their leading `package` declaration and `import` statements (import a.b.C, import a.b.{C, D}, import a.b.{C => E}, import a.b._, import a.b.*, and import a.b.given), resolves each import against the set of packages the project itself declares, and builds the same directory-keyed *core.Graph the Go, TypeScript, Python, Rust, Java and Kotlin adapters produce.
Package scala is the Scala language adapter: it statically scans a project's .scala source files for their leading `package` declaration and `import` statements (import a.b.C, import a.b.{C, D}, import a.b.{C => E}, import a.b._, import a.b.*, and import a.b.given), resolves each import against the set of packages the project itself declares, and builds the same directory-keyed *core.Graph the Go, TypeScript, Python, Rust, Java and Kotlin adapters produce.
lang/typescript
Package typescript is the TypeScript/JavaScript language adapter: it statically scans a project's source files for module specifiers (import, export-from, dynamic import, and require), resolves them against on-disk files and tsconfig path aliases, and builds the same directory-keyed *core.Graph the Go adapter produces.
Package typescript is the TypeScript/JavaScript language adapter: it statically scans a project's source files for module specifiers (import, export-from, dynamic import, and require), resolves them against on-disk files and tsconfig path aliases, and builds the same directory-keyed *core.Graph the Go adapter produces.
lsp
Package lsp implements the slice of the Language Server Protocol depdog needs to surface architecture violations as in-editor diagnostics: a hand-rolled JSON-RPC 2.0 codec over the LSP base protocol (Content-Length framing) and a stdio server around an injected check function.
Package lsp implements the slice of the Language Server Protocol depdog needs to surface architecture violations as in-editor diagnostics: a hand-rolled JSON-RPC 2.0 codec over the LSP base protocol (Content-Length framing) and a stdio server around an injected check function.
mcp
Package mcp implements the slice of the Model Context Protocol depdog needs to let an MCP-capable agent (Claude, Cursor, …) consult the architecture in the loop: a hand-rolled JSON-RPC 2.0 codec over the MCP base protocol (Content-Length framing, mirroring internal/lsp) and a stdio server around an injected read-only handler.
Package mcp implements the slice of the Model Context Protocol depdog needs to let an MCP-capable agent (Claude, Cursor, …) consult the architecture in the loop: a hand-rolled JSON-RPC 2.0 codec over the MCP base protocol (Content-Length framing, mirroring internal/lsp) and a stdio server around an injected read-only handler.
report
Package report renders a core.Result for humans and machines.
Package report renders a core.Result for humans and machines.
tui
Package tui is depdog's Bubble Tea interface: an interactive view over a core.Result.
Package tui is depdog's Bubble Tea interface: an interactive view over a core.Result.
wizard
Package wizard turns a repository's real layout and a chosen architecture preset into a depdog.yaml.
Package wizard turns a repository's real layout and a chosen architecture preset into a depdog.yaml.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL