dma check
Hard dependency graph check for CI — installation, usage, and rule reference.
dma check is the main DMA command. It builds an import graph for the project and validates architectural rules. If something is violated — non-zero exit code, CI fails.
Editor linters are useful but do not replace check: cycles, inbound predicates, and the full graph are visible only to the CLI.
Installation
npm install -D @derived-modular/cliRequires Node.js 18+.
Run only via the package name:
npx @derived-modular/cli check
npx @derived-modular/cli check . # explicit path to project root
npx @derived-modular/cli check --format json
npx @derived-modular/cli check --suggest # safe fix plan (no writes)
npx @derived-modular/cli check --fix # apply safe fixes, then re-checkIn package.json:
{
"scripts": {
"dma-check": "npx @derived-modular/cli check ."
}
}By default the CLI looks for the tree src/{app|pages|routes, features, services?, shared} relative to the path you pass.
Exit codes
| Code | Meaning |
|---|---|
0 | No errors |
1 | Architectural violations found |
2 | Environment failure: missing src/, broken tsconfig, invalid arguments |
In CI this is usually enough:
npx @derived-modular/cli check .For GitHub Actions and agents — --format json (stable report version: 1).
Disable colored output with NO_COLOR=1.
Safe autofix (--suggest / --fix)
Mechanical remediations that do not invent architecture:
| Flag | Behavior |
|---|---|
--suggest | Print a plan; do not write files |
--fix | Apply the plan, then re-run check |
Supported today:
no-barrel— rewrite importers of a single-target barrel to the direct public path (same idea as ESLint autofix). The barrel file itself is not deleted.public-api— rewrite a deep import to mirroredpublic/<same-relative-path>only if that file already exists.
Requires a single project root (not multi-root discover). Mutually exclusive with each other. Does not create public/ files or run promote.
What is checked
| Rule | Meaning |
|---|---|
layer-direction | Imports only downward: app/pages/routes → features → services → shared |
feature-to-feature | A feature must not import another feature |
public-api | Cross-module imports go through */public/* |
no-barrel | No barrel index with re-exports inside modules |
no-cycle | The module graph is acyclic |
feature-has-inbound | A feature with inbound edges from modules → must live in services/ |
service-no-inbound | A service with no consumers among modules → predicate violation |
Inbound edges are counted only from modules (features/*, services/*). Mounts from app/, pages/, routes/ do not trigger promotion.
Rule context — in Four invariants and Layers. The "where to put a file" algorithm — Where to put a file.
Violation reference
Typical ruleId values from the check report and what to do about them.
| ruleId | Cause | Typical fix | More |
|---|---|---|---|
feature-to-feature | Feature imports another feature | Promotion to services/, lift to shared/, or wiring in composition root | Cross-module wiring, Where to put a file |
layer-direction | Import upward in the layer stack (e.g. feature → app/) | Remove the import; pass data via props/events from composition root | Layers |
public-api | Cross-module import bypasses */public/* | Import */public/<file> directly; internal — relative only inside the module | Modules |
no-barrel | index.ts with re-exports inside a module | Remove the barrel; import specific files | Why no barrels |
no-cycle | Cycle in the module graph | Extract to shared/, promotion to services/, or port + bind in app/ | Cross-module wiring |
feature-has-inbound | Feature has inbound from another module | dma promote <name> --apply, then revisit public/ | dma promote · Code evolution |
service-no-inbound | Service with no consumers among modules | Move back to feature colocation or remove empty service | Layers |
Exit code 2 (environment)
| Symptom | What to check |
|---|---|
Missing src/ / no DMA roots | Single-app: point at the app root. Monorepo root: discovery should find apps, or pass --roots / --include-packages |
| Broken tsconfig | paths for @/ in the app tsconfig |
| Invalid arguments | npx @derived-modular/cli check --help |
See Monorepo for multiple app roots.
What the CLI can analyze
Static imports are scanned in:
.ts,.tsx,.js,.jsx,.mjs,.cjs.vue,.svelte,.astro.md,.mdx— imports in content also enter the graph (relevant for doc-apps and MDX components)
import, export … from, import type, dynamic import(), and require() are counted — including targets of next/dynamic and React.lazy.
Path aliases are picked up from tsconfig.json.
Output formats
| Format | When to use |
|---|---|
human (default) | Local development |
json | CI, scripts, AI agents |
sarif | GitHub Code Scanning |
npx @derived-modular/cli check --format sarif > dma.sarifcheck vs doctor
dma check | dma doctor | |
|---|---|---|
| Purpose | Hard rules | Soft evolution signals |
| Exit code on findings | 1 | 0 (default) |
| Where to run | CI, pre-push | Locally |
doctor suggests shared-candidate, stage-growth, dense-services, orphan-public — but does not duplicate check errors. Details on the dma doctor page.
Linters — supplement, not replacement
In the editor you can enable ESLint, Biome, or Oxlint DMA plugins. They catch some rules per file but do not see cycles and inbound predicates.
Always run npx @derived-modular/cli check in CI. Linters are for fast feedback in the IDE.
Overview of all adapters — Tooling overview.
Example for the DMA monorepo
If you are in the derived-modular-architecture repository:
npx @derived-modular/cli check .