depdog

module
v0.4.0 Latest Latest
Warning

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

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

README

depdog

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

Go Reference CI Latest release

Install · Quick start · Configuration · Commands · CI

depdog is a dependency watchdog: architecture rules — "the domain imports nothing but the standard library," "handlers never import repositories" — usually live in someone's head or a wiki, and they rot. depdog makes them executable: you declare which components exist and who may import whom in one small depdog.yaml, and depdog check enforces it against every import edge in your module, exiting non-zero for CI.

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

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      # scan the module and write a starter depdog.yaml
depdog check     # enforce 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-pattern components — or accept as-is with --yes. It refuses to touch an existing depdog.yaml; as the code grows, depdog init --merge rescans the module and appends a component (and, under default: deny, a starter rule) for every directory no existing pattern covers — editing the file in place without disturbing your comments, ordering or formatting. When everything is covered it changes nothing and says so.

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/**", deny: [service, repository] } # forbids its peers
  service:    { path: "internal/service/**", deny: [handler, repository] }
  repository: { path: "internal/repository/**", deny: [handler, service] }

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 a whitelist (an allow list — only what's listed passes) and the three peers are blacklists (a deny list — everything except what's listed); the stance is read per component from which word you use. main has no rule at all, so it falls back to the top-level default — which is allow, so it may import anything (an explicit allow: ["*"] would be equivalent, just noisier). path takes a single glob or a list (path: ["internal/api/**", "internal/rpc/**"]).

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

Components and matching

A component is a named set of packages: each path glob is matched, recursive doublestar style, against module-relative package directories. When patterns overlap, the most specific one wins; equal specificity is an ambiguity error, not a silent pick.

What goes in allow and deny

Entry Matches
domain, handler, … another component, by name
std the Go standard library
external any module that isn't yours
unassigned in-module packages no component claims
"*" everything
golang.org/x/sync one specific external module, by prefix — any entry with / or .

Groups name a reusable set of components: declare groups: { inner: [domain, core] }, then reference inner in any allow/deny list; it expands to its members when the config loads.

Two rules of precedence to remember: an explicit deny always beats an allow, and a component with neither falls back to the top-level default — set default: deny to make unruled components fail closed (init asks which stance you want).

Boundaries

Components answer "who may this layer import?" along one axis. Boundaries add a second, orthogonal axis: named sets of members that may not import across each other. A package keeps its most-specific component and, independently, belongs to every boundary whose region contains it — so cmd/service-a/services/x can be the service-a-services component (subject to layer rules) and a member of the cmd-services boundary (subject to isolation) at once. That dissolves two kinds of boilerplate: peer deny lists ("layers don't import each other") and cross-cutting isolation ("no service imports another"), which otherwise needs O(n²) deny lists.

boundaries:
  # shorthand — a symmetric peer set; these three may not import each other
  service-a-layers: [service-a-repositories, service-a-services, service-a-handlers]

  # expanded form — members can be path globs, and sealed adds a one-way wall
  cmd-services:
    members: ["cmd/service-a/**", "cmd/service-b/**"]
    sealed: true

A member is a component name or a path glob (told apart by the same /-or- metacharacter heuristic as allow/deny refs); the two may mix in one boundary.

edge verdict
member A → member B (A ≠ B) denied (a hard deny — wins over any component allow)
within one member (incl. same package) allowed
member → ungrouped (e.g. a shared lib) allowed
ungrouped → member allowed — denied when the boundary is sealed

sealed: true adds one rule: nothing outside all members may import into a member. The wall is one-way, so a service may still import a shared lib, but a shared lib (or another service) must not reach in. Boundaries are composable (each edge is checked against every boundary plus the component rules) and orthogonal to assignment — membership never silences the unassigned warning for a package no component claims. explain reports a crossing as denied by boundary "cmd-services", with (sealed) for the one-way rule.

Signals that never fail the build

Three findings are always reported but never exit non-zero on their own — visibility without blocking adoption:

  • Unmapped packages. In-module packages no component claims are warnings; unmapped packages are how rule sets rot, so they stay visible.
  • Dead patterns. A component whose patterns match no package is flagged — a likely typo.
  • Component cycles. a ↔ b at the architecture level (which a package-level compile check can't even have) is detected and reported as an advisory.

Test files

test_files: hybrid (the default) lets _test.go files import any external module while still enforcing component-to-component rules; same-rules is strict, relaxed exempts test files entirely.

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 baseline Record current violations to depdog.baseline.yaml for the ratchet
depdog graph Emit the dependency graph as DOT or Mermaid
depdog explain <component-or-package> [import] Explain why something is red (the rule or boundary that fired, with file:line), how a component is constrained, its boundary membership, or whether A may import B and which rule or boundary decides it
depdog config Print the compiled rule set — components, patterns, inferred stances, boundaries, options — for debugging a config
depdog tui (or bare depdog) Interactive terminal UI: component dashboard, browsable violations, per-package imports and importers, and a Config tab showing the compiled rules
All flags
Command Flags
init --preset ddd|hexagonal|layered|flat · --default deny|allow · --yes (non-interactive) · --force (overwrite) · --merge (extend an existing file, preserving comments and formatting)
check --format text|json|github|sarif · --fail-on any|new · --color auto|always|never
graph --format dot|mermaid · --level component|package · --violations-only · --focus <component>

In the TUI, 1–4 (or tab) switch between the Dashboard, Violations, Packages and Config screens. The Violations and Packages lists scroll and filter with /; e opens the selection in $EDITOR at its file:line, r re-runs the check in place, and ? shows all keys. The Config tab (4) shows the active config path and the compiled rule set (the same data as depdog config); e there opens depdog.yaml in $EDITOR, and the editor exiting auto-re-runs the check so the edited rules take effect on every screen.

Exit codes are a contract:

Code Meaning
0 clean
1 violations
2 configuration or usage error

CI

depdog check is CI-ready as-is. For inline pull-request annotations use the GitHub format; for GitHub code scanning, emit 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 }

Adopting rules on 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

depdog checks itself

depdog's own architecture is declared in its depdog.yaml and enforced in CI: the language-agnostic engine (internal/core) depends on the standard library only, language knowledge lives behind an adapter interface, and the layers above may only import inward. A failing architecture is a failing build.

Multi-language support

depdog checks Go and TypeScript/JavaScript projects with the same depdog.yaml, the same commands (check, graph, explain, config, TUI), and the same engine. Only a thin language adapter differs; the rule format is neutral — component path globs match module-relative directories, and std / external are abstract buckets each adapter fills (Go stdlib vs Node builtins; a Go module vs an node_modules package). The TypeScript adapter is a pure-Go static import scanner: no Node.js or tsc is required, depdog stays a single binary.

Auto-detection. depdog picks the adapter from the project's marker files, walking up from the working directory:

  • a go.mod ⇒ the Go adapter;
  • a tsconfig.json or package.json ⇒ the TypeScript/JS adapter;
  • the marker nearest the working directory wins in a nested layout.

Explicit override. The persistent --lang go|ts flag (available to every subcommand) bypasses detection:

depdog check --lang ts        # force the TypeScript adapter
depdog graph --lang go        # force the Go adapter

A directory that carries both a go.mod and a tsconfig.json/package.json with no --lang is genuinely ambiguous: depdog exits with a usage error naming --lang rather than silently guessing.

Limitations

  • One build configuration. depdog 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.
  • Single module. Go workspaces (go.work) aren't supported: depdog checks one module and declines to run inside a workspace with a clear message (set GOWORK=off to bypass a workspace and check the module directly).

Status

v0.2.0 — the current release. It brings the config v2 format (per-component allow/deny in one block, default stance) and a second language adapter (TypeScript/JavaScript, selected by auto-detect or --lang) among a round of post-v0.1 refinements. The M0–M5 roadmap in PLAN.md is complete; BACKLOG.md tracks what's next.

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/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/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.
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