Derived Modular Arch
Tooling

dma doctor

Soft evolution signals for local work — without failing CI.

dma doctor inspects the same graph as check, but looks for growth signals, not hard violations. By default exit code is 0 — you can run it on every commit without breaking CI.

Think of doctor as an "architectural linter ahead of time": it hints at what to extract, promote, or split — before check starts complaining.

Usage

npx @derived-modular/cli doctor .
npx @derived-modular/cli doctor . --format json
npx @derived-modular/cli doctor . --format sarif
npx @derived-modular/cli doctor . --suggest   # plan orphan-public deletes
npx @derived-modular/cli doctor . --fix      # delete orphan public files

Same output formats as check: human, json, SARIF.

doctor vs check

dma checkdma doctor
PurposeHard rulesEvolution signals
Blocks CIYes (exit 1)No (exit 0)
Repeats rule IDs from checkNo
When to runCI, pre-pushLocally, once per sprint

Doctor does not duplicate check errors. If check is red — fix check first.

Signals

shared-candidate

A module file is imported by 2+ other modules.

help names the next placement step by the file's current layer:

  • under features/ → promote to services/<name>/public/ (Placement #2)
  • under services/ → keep if product flow, else extract to shared/{ui,lib,…} (Placement #2–3)
  • under shared/ → already shared; keep portability intentional (Placement #3)

Do not lift to shared automatically — understand the code first.

stage-growth

Module structure lags behind size. help ties the fix to placement colocation:

  • file-module (stage 0) gained sibling files with the same basename prefix (e.g. checkout.tsx + checkout.store.ts) → folder + public/ (Placement #4–5)
  • many files without segments → add ui/, model/, api/ (Placement #4)

Default threshold is about 8 files (stage1FileCount).

Example: stage-growth on checkout

Before — stage 0, one UI file, but a store with the same prefix appeared next to it:

features/
├── checkout.tsx
└── checkout.store.ts

dma doctorstage-growth: file-module checkout gained a sibling with prefix checkout.

After — stage 1:

features/checkout/
├── public/
│   └── checkout-page.tsx
├── checkout.store.ts
└── checkout.shipping.ts
  1. Create folder features/checkout/
  2. Move UI to public/checkout-page.tsx
  3. Store and helpers stay internal; public/ imports them relatively
  4. Update imports in composition root to @/features/checkout/public/checkout-page
  5. npx @derived-modular/cli check . — green; npx @derived-modular/cli doctor .stage-growth for checkout gone

If files reach ~8+ without segments — add ui/, model/, api/ (stage 2). See Modules and public API.

dense-services

The services/ subgraph looks dense or deep. Time to split horizontally — domains, packages — not add new vertical layers.

orphan-public

A file in public/ is imported by nobody. Dead contract — remove it or make it internal.

--suggest / --fix can delete such files only when the graph shows no importers at all (including inside the module). Single project root only.

Typical workflow

# morning
npx @derived-modular/cli check .          # gate

# before refactor
npx @derived-modular/cli doctor .         # what to restructure

# after a large PR
npx @derived-modular/cli doctor . --format json > doctor.json

Do not use doctor as a CI gate

Signals are subjective: shared-candidate on healthy services/*/public/* is normal. Use doctor for team conversation, not to block merge.

In CI, doctor only makes sense as an informational job (json artifact for a dashboard).

What's next

On this page