Derived Modular Arch
Tooling

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

Requires 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-check

In 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

CodeMeaning
0No errors
1Architectural violations found
2Environment 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:

FlagBehavior
--suggestPrint a plan; do not write files
--fixApply 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 mirrored public/<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

RuleMeaning
layer-directionImports only downward: app/pages/routes → features → services → shared
feature-to-featureA feature must not import another feature
public-apiCross-module imports go through */public/*
no-barrelNo barrel index with re-exports inside modules
no-cycleThe module graph is acyclic
feature-has-inboundA feature with inbound edges from modules → must live in services/
service-no-inboundA 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.

ruleIdCauseTypical fixMore
feature-to-featureFeature imports another featurePromotion to services/, lift to shared/, or wiring in composition rootCross-module wiring, Where to put a file
layer-directionImport upward in the layer stack (e.g. feature → app/)Remove the import; pass data via props/events from composition rootLayers
public-apiCross-module import bypasses */public/*Import */public/<file> directly; internal — relative only inside the moduleModules
no-barrelindex.ts with re-exports inside a moduleRemove the barrel; import specific filesWhy no barrels
no-cycleCycle in the module graphExtract to shared/, promotion to services/, or port + bind in app/Cross-module wiring
feature-has-inboundFeature has inbound from another moduledma promote <name> --apply, then revisit public/dma promote · Code evolution
service-no-inboundService with no consumers among modulesMove back to feature colocation or remove empty serviceLayers

Exit code 2 (environment)

SymptomWhat to check
Missing src/ / no DMA rootsSingle-app: point at the app root. Monorepo root: discovery should find apps, or pass --roots / --include-packages
Broken tsconfigpaths for @/ in the app tsconfig
Invalid argumentsnpx @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

FormatWhen to use
human (default)Local development
jsonCI, scripts, AI agents
sarifGitHub Code Scanning
npx @derived-modular/cli check --format sarif > dma.sarif

check vs doctor

dma checkdma doctor
PurposeHard rulesSoft evolution signals
Exit code on findings10 (default)
Where to runCI, pre-pushLocally

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 .

What's next

On this page