depdog

module
v0.1.0 Latest Latest
Warning

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

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

README

depdog

Keep a Go codebase's internal dependencies pointing in the right direction.

Architecture rules — "the domain imports nothing but the standard library", "handlers never import repositories" — usually live in people's heads or a wiki, and they rot. depdog makes them executable: you declare which components exist and who may import whom in a small depdog.yaml, and depdog check enforces it against every import edge in the module, with a non-zero exit code for CI.

depdog check — github.com/acme/shop

✗ domain: allow [std]  (2 violations)
    github.com/acme/shop/internal/domain/order
      → github.com/acme/shop/internal/repository   internal/domain/order/order.go:7
      → github.com/google/uuid                     internal/domain/order/order.go:9

1 violation · 1 warning · 12 packages · 39 edges checked in 40ms

Install

# From source (Go 1.26+):
go install github.com/matterpale/depdog/cmd/depdog@latest

# Or build the repo directly:
go build -o depdog ./cmd/depdog

Prebuilt binaries for Linux, macOS, and Windows are on the releases page.

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.

When a depdog.yaml already exists, init refuses to touch it. As the code grows, depdog init --merge rescans the module and appends a component (and, under policy: 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: 1

components:
  main:       ["cmd/**"]
  domain:     ["internal/domain/**"]
  handler:    ["internal/handler/**"]
  service:    ["internal/service/**"]
  repository: ["internal/repository/**"]

policy: deny          # whitelist stance — only what a rule allows may be imported

rules:
  main:       { allow: ["*"] }                 # the entrypoint wires everything
  domain:     { allow: [std] }                 # the pure core: std-lib only
  handler:    { allow: [domain, std, external] }
  service:    { allow: [domain, std, external] }
  repository: { allow: [domain, std, external] }

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

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

Key ideas:

  • Components are named sets of packages, matched by recursive doublestar globs against module-relative package dirs. When patterns overlap, the most specific wins; equal specificity is an ambiguity error.
  • Stance is inferred per rule from word choice. A rule with an allow list is a whitelist (only the listed imports pass); a rule with only a deny list is a blacklist (everything passes except what's listed). An explicit deny always beats an allow. This lets stances mix per component — handler: { deny: [repository] } means "anything but repository" even when other components are strict whitelists.
  • policy is the fallback for components with no allow/deny rule: deny (whitelist) or allow (blacklist). It is optional — omit it for the strict deny default. init asks which you want.
  • Allow/deny entries are component names or the specials std, external, unassigned and "*". An entry that looks like an import path (contains / or .) restricts a specific external module by prefix, e.g. allow: [std, "golang.org/x/sync"] permits std and that one dependency; list it under deny to forbid just that module.
  • 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.
  • In-module packages no component claims are always reported as warnings, but never fail the build on their own — unmapped packages are how rule sets rot, so they stay visible without blocking adoption. A component whose patterns match no package is likewise flagged (a likely typo or dead pattern).
  • Component import cycles (a ↔ b at the architecture level — which a package-level compile check can't even have) are detected and reported as an advisory, never failing the build on their own.
  • 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. --preset ddd|hexagonal|layered|flat, --policy deny|allow, --yes (non-interactive), --force (overwrite), --merge (add components for uncovered directories to an existing file, preserving its comments and formatting).
depdog check [packages] Evaluate imports against the rules. --format text|json|github|sarif, --fail-on any|new, --color auto|always|never.
depdog baseline Record current violations to depdog.baseline.yaml for the ratchet below.
depdog graph Emit the dependency graph. --format dot|mermaid, --level component|package, --violations-only, --focus <component>.
depdog config Print the compiled rule set — components, patterns, each component's inferred stance and rule, the policy and options — for debugging a config.
depdog explain <component-or-package> [import] Explain why something is red (the rule that fired, with file:line), how a component is constrained, or — with a second argument — whether one package may import another (a package, component, or external module) and which rule decides it.
depdog tui / bare depdog Interactive terminal UI: a component dashboard, a browsable violations list, and per-package imports/importers. 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; ? shows all keys.

Exit codes are a contract: 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.

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.1.0 — the first tagged release. The M0–M5 roadmap in PLAN.md is complete; BACKLOG.md tracks what's next.

License

MIT

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