Skip to main content

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
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".

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 / .jsx projects (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   = []

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"

[[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

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, ...)
    │ 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mille-0.0.11.tar.gz (1.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mille-0.0.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.5 MB view details)

Uploaded CPython 3.8manylinux: glibc 2.17+ x86-64

File details

Details for the file mille-0.0.11.tar.gz.

File metadata

  • Download URL: mille-0.0.11.tar.gz
  • Upload date:
  • Size: 1.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: maturin/1.12.6

File hashes

Hashes for mille-0.0.11.tar.gz
Algorithm Hash digest
SHA256 025f4ab95741c0521a0ec62102d4e18ce7fec8130c5ca52c1aace64eca379bfd
MD5 276e58edb4f14e35b5198183eec42d54
BLAKE2b-256 109360f4d3ffe9ed2155bb689bd53466d7dada21de5dce2e9e0e56e2b20b2676

See more details on using hashes here.

File details

Details for the file mille-0.0.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for mille-0.0.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 b996b48159b72f08485232b151611238219bc144470905f56107cab436c05c34
MD5 40e5d43037654e3be9b570864fefe5fb
BLAKE2b-256 d39880e53256f465a89fd21572de1ea99bebf79a29e6a4061079109bcf71033e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page