Architecture Checker — static analysis for layered architecture rules
Project description
mille
Like a mille crêpe — your architecture, one clean layer at a time.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ presentation
· · · · · · · · · · · · · · · · · · (deps only flow inward)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ infrastructure
· · · · · · · · · · · · · · · · · ·
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ usecase
· · · · · · · · · · · · · · · · · ·
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ domain
mille is a static analysis CLI that enforces dependency rules for layered architectures — Clean Architecture, Onion Architecture, Hexagonal Architecture, and more.
One TOML config. Rust-powered. CI-ready. Supports multiple languages from a single config file.
What it checks
| Check | Rust | Go | TypeScript | JavaScript | Python | Java | Kotlin |
|---|---|---|---|---|---|---|---|
Layer dependency rules (dependency_mode) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
External library rules (external_mode) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
DI method call rules (allow_call_patterns) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Install
cargo
cargo install mille
npm
npm install -g @makinzm/mille
mille check
Or without installing globally:
npx @makinzm/mille check
Requires Node.js ≥ 18. Bundles mille.wasm — no native compilation needed.
go install
go install github.com/makinzm/mille/packages/go/mille@latest
Embeds mille.wasm via wazero — fully self-contained binary.
pip / uv
# uv (recommended)
uv add --dev mille
uv run mille check
# pip
pip install mille
mille check
Binary download
Pre-built binaries are on GitHub Releases:
| Platform | Archive |
|---|---|
| Linux x86_64 | mille-<version>-x86_64-unknown-linux-gnu.tar.gz |
| Linux arm64 | mille-<version>-aarch64-unknown-linux-gnu.tar.gz |
| macOS x86_64 | mille-<version>-x86_64-apple-darwin.tar.gz |
| macOS arm64 | mille-<version>-aarch64-apple-darwin.tar.gz |
| Windows x86_64 | mille-<version>-x86_64-pc-windows-msvc.zip |
Quick Start
1. Generate mille.toml with mille init
mille init
mille init analyzes actual import statements in your source files to infer layer structure and dependencies — no predetermined naming conventions needed. It prints the inferred dependency graph before writing the config:
Detected languages: rust
Scanning imports...
Using layer depth: 2
Inferred layer structure:
domain ← (no internal dependencies)
usecase → domain
external: anyhow
infrastructure → domain
external: serde, tokio
Generated 'mille.toml'
| Flag | Default | Description |
|---|---|---|
--output <path> |
mille.toml |
Write config to a custom path |
--force |
false | Overwrite an existing file without prompting |
--depth <N> |
auto | Layer detection depth from project root |
--depth and auto-detection: mille init automatically finds the right layer depth by trying depths 1–6, skipping common source-layout roots (src, lib, app, etc.), and selecting the first depth that yields 2–8 candidate layers. For a project with src/domain/entity, src/domain/repository, src/usecase/ — depth 2 is chosen, rolling entity and repository up into domain. Use --depth N to override when auto-detection picks the wrong level.
The generated config includes allow (inferred internal dependencies) and external_allow (detected external packages) per layer. After generating, review the config and run mille check to see results.
Naming in monorepos: When multiple sub-projects contain a directory with the same name (e.g. crawler/src/domain and server/src/domain), mille init gives each its own layer with a distinguishing prefix (crawler_domain, server_domain). Merging is left to you.
Excluded paths: mille check automatically skips .venv, venv, node_modules, target, dist, build, and similar build/dependency directories, so generated paths patterns like apps/** are safe to use.
Python submodule imports: external_allow = ["matplotlib"] correctly allows both import matplotlib and import matplotlib.pyplot.
Go projects: mille init reads go.mod and generates [resolve.go] module_name automatically — internal module imports are classified correctly during mille check. External packages appear in external_allow with their full import paths (e.g. "github.com/cilium/ebpf", "fmt", "net/http").
TypeScript/JavaScript subpath imports: external_allow = ["vitest"] correctly allows both import "vitest" and import "vitest/config". Scoped packages (@scope/name/sub) are matched by "@scope/name".
Java/Kotlin projects: mille init uses package declarations — not directory depth — to detect layers. This works correctly for Maven's src/main/java/com/example/myapp/domain/ as well as flat src/domain/ layouts. pom.xml (Maven) and build.gradle + settings.gradle (Gradle) are read automatically to generate [resolve.java] module_name. Layer paths use **/layer/** globs so mille check matches regardless of the source root depth.
Detected languages: java
Scanning imports...
Inferred layer structure:
domain ← (no internal dependencies)
infrastructure → domain
external: java.util.List
usecase → domain
main → domain, infrastructure, usecase
Generated 'mille.toml'
2. (Or) Create mille.toml manually
Place mille.toml in your project root:
Rust:
[project]
name = "my-app"
root = "."
languages = ["rust"]
[[layers]]
name = "domain"
paths = ["src/domain/**"]
dependency_mode = "opt-in"
allow = []
external_mode = "opt-in"
external_allow = []
[[layers]]
name = "usecase"
paths = ["src/usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-in"
external_allow = []
[[layers]]
name = "infrastructure"
paths = ["src/infrastructure/**"]
dependency_mode = "opt-out"
deny = []
external_mode = "opt-out"
external_deny = []
[[layers]]
name = "main"
paths = ["src/main.rs"]
dependency_mode = "opt-in"
allow = ["domain", "infrastructure", "usecase"]
external_mode = "opt-in"
external_allow = ["clap"]
[[layers.allow_call_patterns]]
callee_layer = "infrastructure"
allow_methods = ["new", "build", "create", "init", "setup"]
TypeScript / JavaScript:
[project]
name = "my-ts-app"
root = "."
languages = ["typescript"]
[resolve.typescript]
tsconfig = "./tsconfig.json"
[[layers]]
name = "domain"
paths = ["domain/**"]
dependency_mode = "opt-in"
allow = []
external_mode = "opt-out"
external_deny = []
[[layers]]
name = "usecase"
paths = ["usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-in"
external_allow = ["zod"]
[[layers]]
name = "infrastructure"
paths = ["infrastructure/**"]
dependency_mode = "opt-out"
deny = []
external_mode = "opt-out"
external_deny = []
Use
languages = ["javascript"]for plain.js/.jsxprojects (no[resolve.typescript]needed).
Go:
[project]
name = "my-go-app"
root = "."
languages = ["go"]
[resolve.go]
module_name = "github.com/myorg/my-go-app"
[[layers]]
name = "domain"
paths = ["domain/**"]
dependency_mode = "opt-in"
allow = []
[[layers]]
name = "usecase"
paths = ["usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
[[layers]]
name = "infrastructure"
paths = ["infrastructure/**"]
dependency_mode = "opt-out"
deny = []
[[layers]]
name = "cmd"
paths = ["cmd/**"]
dependency_mode = "opt-in"
allow = ["domain", "usecase", "infrastructure"]
Python:
[project]
name = "my-python-app"
root = "."
languages = ["python"]
[resolve.python]
src_root = "."
package_names = ["domain", "usecase", "infrastructure"]
[[layers]]
name = "domain"
paths = ["domain/**"]
dependency_mode = "opt-in"
allow = []
external_mode = "opt-out"
external_deny = []
[[layers]]
name = "usecase"
paths = ["usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-out"
external_deny = []
[[layers]]
name = "infrastructure"
paths = ["infrastructure/**"]
dependency_mode = "opt-out"
deny = []
external_mode = "opt-out"
external_deny = []
Java / Kotlin:
[project]
name = "my-java-app"
root = "."
languages = ["java"] # or ["kotlin"] for Kotlin projects
[resolve.java]
module_name = "com.example.myapp"
[[layers]]
name = "domain"
paths = ["src/domain/**"]
dependency_mode = "opt-in"
allow = []
external_mode = "opt-out"
[[layers]]
name = "usecase"
paths = ["src/usecase/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-out"
[[layers]]
name = "infrastructure"
paths = ["src/infrastructure/**"]
dependency_mode = "opt-in"
allow = ["domain"]
external_mode = "opt-in"
external_allow = ["java.util.List", "java.util.Map"]
module_nameis the base package of your project (e.g.com.example.myapp). Imports starting with this prefix are classified as Internal and matched against layer globs. All other imports (includingjava.util.*stdlib) are classified as External and subject toexternal_allow/external_denyrules.
2. Visualize with mille analyze
Before enforcing rules, you can inspect the actual dependency graph:
mille analyze # human-readable terminal output (default)
mille analyze --format json # machine-readable JSON graph
mille analyze --format dot # Graphviz DOT (pipe to: dot -Tsvg -o graph.svg)
mille analyze --format svg # self-contained SVG image (open in a browser)
Example SVG output (dark theme, green edges):
mille analyze --format svg > graph.svg && open graph.svg
mille analyze always exits 0 — it only visualizes, never enforces rules.
3. Inspect external dependencies with mille report external
mille report external # human-readable table (default)
mille report external --format json # machine-readable JSON
mille report external --output report.json --format json # write to file
Shows which external packages each layer actually imports — useful for auditing external_allow lists or documenting your dependency footprint.
Example output:
External Dependencies by Layer
domain (none)
usecase (none)
infrastructure database/sql
cmd fmt, os
mille report external always exits 0 — it only reports, never enforces rules.
4. Run mille check
mille check
Output formats:
mille check # human-readable terminal output (default)
mille check --format github-actions # GitHub Actions annotations (::error file=...)
mille check --format json # machine-readable JSON
Fail threshold:
mille check # exit 1 on error-severity violations only (default)
mille check --fail-on warning # exit 1 on any violation (error or warning)
mille check --fail-on error # explicit default — same as no flag
Exit codes:
| Code | Meaning |
|---|---|
0 |
No violations (or only warnings without --fail-on warning) |
1 |
One or more violations at the configured fail threshold |
3 |
Configuration file error |
Configuration Reference
[project]
| Key | Description |
|---|---|
name |
Project name |
root |
Root directory for analysis |
languages |
Languages to check: "rust", "go", "typescript", "javascript", "python", "java", "kotlin" |
[[layers]]
| Key | Description |
|---|---|
name |
Layer name |
paths |
Glob patterns for files in this layer |
dependency_mode |
"opt-in" (deny all except allow) or "opt-out" (allow all except deny) |
allow |
Allowed layers (when dependency_mode = "opt-in") |
deny |
Forbidden layers (when dependency_mode = "opt-out") |
external_mode |
"opt-in" or "opt-out" for external library usage |
external_allow |
Allowed external packages (when external_mode = "opt-in") |
external_deny |
Forbidden external packages (when external_mode = "opt-out") |
[[layers.allow_call_patterns]]
Restricts which methods may be called on a given layer's types. Only valid on the main layer (or equivalent DI entrypoint).
| Key | Description |
|---|---|
callee_layer |
The layer whose methods are being restricted |
allow_methods |
List of method names that are permitted |
[ignore]
Exclude files from the architecture check entirely, or suppress violations for test/mock files.
| Key | Description |
|---|---|
paths |
Glob patterns — matching files are excluded from collection and not counted in layer stats |
test_patterns |
Glob patterns — matching files are still counted in layer stats but their imports are not violation-checked |
[ignore]
paths = ["**/mock/**", "**/generated/**", "**/testdata/**"]
test_patterns = ["**/*_test.go", "**/*.spec.ts", "**/*.test.ts"]
When to use paths vs test_patterns:
paths: Files that should not be analyzed at all (generated code, vendor directories, mocks)test_patterns: Test files that intentionally import across layers (e.g., integration tests that import both domain and infrastructure)
[severity]
Control the severity level of each violation type. Violations can be "error", "warning", or "info".
| Key | Default | Description |
|---|---|---|
dependency_violation |
"error" |
Layer dependency rule violated |
external_violation |
"error" |
External library rule violated |
call_pattern_violation |
"error" |
DI entrypoint method call rule violated |
unknown_import |
"warning" |
Import that could not be classified |
[severity]
dependency_violation = "warning" # treat as warning for gradual adoption
external_violation = "error"
call_pattern_violation = "error"
unknown_import = "warning"
Use --fail-on warning to exit 1 even for warnings when integrating into CI gradually.
[resolve.typescript]
| Key | Description |
|---|---|
tsconfig |
Path to tsconfig.json. mille reads compilerOptions.paths and resolves path aliases (e.g. @/*) as internal imports. |
How TypeScript / JavaScript imports are classified:
| Import | Classification |
|---|---|
import X from "./module" |
Internal |
import X from "../module" |
Internal |
import X from "@/module" (path alias in tsconfig.json) |
Internal |
import X from "react" |
External |
import fs from "node:fs" |
External |
[resolve.go]
| Key | Description |
|---|---|
module_name |
Go module name (matches go.mod). mille init generates this automatically from go.mod. |
[resolve.python]
| Key | Description |
|---|---|
src_root |
Root directory of the Python source tree (relative to mille.toml) |
package_names |
Your package names — imports starting with these are classified as internal. e.g. ["domain", "usecase"] |
How Python imports are classified:
| Import | Classification |
|---|---|
from .sibling import X (relative) |
Internal |
import domain.entity (matches package_names) |
Internal |
import os, import sqlalchemy |
External |
[resolve.java]
| Key | Description |
|---|---|
module_name |
Base package of your project (e.g. com.example.myapp). Imports starting with this prefix are classified as Internal. Generated automatically by mille init. |
pom_xml |
Path to pom.xml (relative to mille.toml). groupId.artifactId is used as module_name when module_name is not set. |
build_gradle |
Path to build.gradle (relative to mille.toml). group + rootProject.name from settings.gradle is used as module_name when module_name is not set. |
How Java imports are classified:
| Import | Classification |
|---|---|
import com.example.myapp.domain.User (starts with module_name) |
Internal |
import static com.example.myapp.util.Helper.method |
Internal |
import java.util.List, import org.springframework.* |
External |
Both regular and static imports are supported. Wildcard imports (
import java.util.*) are not yet extracted by the parser.
How it Works
mille uses tree-sitter for AST-based import extraction — no regex heuristics.
mille.toml
│
▼
Layer definitions
│
Source files (*.rs, *.go, *.py, *.ts, *.js, *.java, ...)
│ tree-sitter parse
▼
RawImport list
│ Resolver (stdlib / internal / external)
▼
ResolvedImport list
│ ViolationDetector
▼
Violations → terminal output
Dogfooding
mille checks its own source code on every CI run:
mille check # uses ./mille.toml
See mille.toml for the architecture rules applied to mille itself.
Documentation
- spec.md — Full specification (in Japanese)
- docs/TODO.md — Development roadmap
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mille-0.0.12.tar.gz.
File metadata
- Download URL: mille-0.0.12.tar.gz
- Upload date:
- Size: 1.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cbdc61e21dcd2afdfadfca94f275b3412d86d31f4246d7d0280d952775a477a1
|
|
| MD5 |
e8c255f5a345fc718cc81a491b8284b6
|
|
| BLAKE2b-256 |
eebec2b994f374686bcc8b7cb54fba31cbb4a450ef94f9e5fb3605535a7c4764
|
File details
Details for the file mille-0.0.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: mille-0.0.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.9 MB
- Tags: CPython 3.8, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: maturin/1.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6c8f61f83845b6e9355120c37d68fdbd476d766efee95c2e48c1b72d89960e1
|
|
| MD5 |
1041658720e0f4ea949d86ae6bbd223e
|
|
| BLAKE2b-256 |
2d8cd5ccc5dedaccc4131ed3f54e281e9f62b855c8ac7d6cc2036708c1fd28db
|