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 filesSame output formats as check: human, json, SARIF.
doctor vs check
dma check | dma doctor | |
|---|---|---|
| Purpose | Hard rules | Evolution signals |
| Blocks CI | Yes (exit 1) | No (exit 0) |
| Repeats rule IDs from check | — | No |
| When to run | CI, pre-push | Locally, 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 toservices/<name>/public/(Placement #2) - under
services/→ keep if product flow, else extract toshared/{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.tsdma doctor → stage-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- Create folder
features/checkout/ - Move UI to
public/checkout-page.tsx - Store and helpers stay internal;
public/imports them relatively - Update imports in composition root to
@/features/checkout/public/checkout-page npx @derived-modular/cli check .— green;npx @derived-modular/cli doctor .—stage-growthfor 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.jsonDo 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).
Related topics
- Code evolution — second-use, promotion, triggers
- dma check — hard rules
- Tooling overview — hybrid model