@makinzm/mille 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +152 -123
  2. package/index.js +10 -0
  3. package/mille.wasm +0 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,71 +1,75 @@
1
1
  # mille
2
2
 
3
- > Architecture Checkerstatic analysis CLI for layered architecture rules
3
+ > Like a mille crêpe your architecture, one clean layer at a time.
4
4
 
5
- `mille` is a CLI tool that enforces **dependency rules for layered architectures** (Clean Architecture, Onion Architecture, Hexagonal Architecture, etc.).
6
-
7
- It is implemented in Rust, supports multiple languages from a single TOML config, and is designed to run in CI/CD pipelines.
5
+ ```
6
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ presentation
7
+ · · · · · · · · · · · · · · · · · · (deps only flow inward)
8
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ infrastructure
9
+ · · · · · · · · · · · · · · · · · ·
10
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ usecase
11
+ · · · · · · · · · · · · · · · · · ·
12
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ domain
13
+ ```
8
14
 
9
- ## Features
15
+ `mille` is a static analysis CLI that enforces **dependency rules for layered architectures** — Clean Architecture, Onion Architecture, Hexagonal Architecture, and more.
10
16
 
11
- | Feature | Status |
12
- |---|---|
13
- | Internal layer dependency check (`dependency_mode`) | ✅ |
14
- | External library dependency check (`external_mode`) | ✅ |
15
- | DI entrypoint method call check (`allow_call_patterns`) | ✅ |
16
- | Rust support | ✅ |
17
- | Go support | ✅ |
18
- | Python support | ✅ |
19
- | TypeScript / JavaScript support | ✅ |
17
+ One TOML config. Rust-powered. CI-ready. Supports multiple languages from a single config file.
20
18
 
21
- ## How to Install
19
+ ## What it checks
22
20
 
23
- ### cargo (Rust users)
21
+ | Check | Rust | Go | TypeScript | JavaScript | Python |
22
+ |---|:---:|:---:|:---:|:---:|:---:|
23
+ | Layer dependency rules (`dependency_mode`) | ✅ | ✅ | ✅ | ✅ | ✅ |
24
+ | External library rules (`external_mode`) | ✅ | ✅ | ✅ | ✅ | ✅ |
25
+ | DI method call rules (`allow_call_patterns`) | ✅ | ✅ | ✅ | ✅ | ✅ |
24
26
 
25
- ```sh
26
- cargo install mille
27
- ```
27
+ ## Install
28
28
 
29
- ### pip / uv (Python users)
29
+ ### cargo
30
30
 
31
31
  ```sh
32
- # uv (recommended)
33
- uv add --dev mille
34
- uv run mille check
35
-
36
- # pip
37
- pip install mille
38
- mille check
32
+ cargo install mille
39
33
  ```
40
34
 
41
- The Python package is a native extension built with [maturin](https://github.com/PyO3/maturin) (PyO3). It provides both a CLI (`mille check`) and a Python API (`import mille; mille.check(...)`).
42
-
43
- ### npm (Node.js users)
35
+ ### npm
44
36
 
45
37
  ```sh
46
38
  npm install -g @makinzm/mille
47
39
  mille check
48
40
  ```
49
41
 
50
- Or use it without installing globally:
42
+ Or without installing globally:
51
43
 
52
44
  ```sh
53
45
  npx @makinzm/mille check
54
46
  ```
55
47
 
56
- Requires Node.js ≥ 18. The npm package bundles `mille.wasm` (the compiled Rust core) and runs it via Node.js's built-in `node:wasi` module — no native compilation or network access required at install time.
48
+ Requires Node.js ≥ 18. Bundles `mille.wasm` — no native compilation needed.
57
49
 
58
50
  ### go install
59
51
 
60
52
  ```sh
61
- go install github.com/makinzm/mille/packages/go@latest
53
+ go install github.com/makinzm/mille/packages/go/mille@latest
62
54
  ```
63
55
 
64
- The Go wrapper embeds `mille.wasm` (the compiled Rust core) and runs it via [wazero](https://wazero.io/) — a zero-dependency WebAssembly runtime. No network access or caching required; the binary is fully self-contained.
56
+ Embeds `mille.wasm` via [wazero](https://wazero.io/) — fully self-contained binary.
65
57
 
66
- ### Direct binary download
58
+ ### pip / uv
67
59
 
68
- Pre-built binaries for each platform are available on [GitHub Releases](https://github.com/makinzm/mille/releases):
60
+ ```sh
61
+ # uv (recommended)
62
+ uv add --dev mille
63
+ uv run mille check
64
+
65
+ # pip
66
+ pip install mille
67
+ mille check
68
+ ```
69
+
70
+ ### Binary download
71
+
72
+ Pre-built binaries are on [GitHub Releases](https://github.com/makinzm/mille/releases):
69
73
 
70
74
  | Platform | Archive |
71
75
  |---|---|
@@ -75,19 +79,46 @@ Pre-built binaries for each platform are available on [GitHub Releases](https://
75
79
  | macOS arm64 | `mille-<version>-aarch64-apple-darwin.tar.gz` |
76
80
  | Windows x86_64 | `mille-<version>-x86_64-pc-windows-msvc.zip` |
77
81
 
82
+ ## Quick Start
83
+
84
+ ### 1. Generate `mille.toml` with `mille init`
85
+
78
86
  ```sh
79
- # Example: Linux x86_64
80
- curl -L https://github.com/makinzm/mille/releases/latest/download/mille-<version>-x86_64-unknown-linux-gnu.tar.gz | tar xz
81
- ./mille check
87
+ mille init
82
88
  ```
83
89
 
84
- ## Quick Start
90
+ `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:
85
91
 
86
- ### 1. Create `mille.toml`
92
+ ```
93
+ Detected languages: rust
94
+ Scanning imports...
95
+ Using layer depth: 2
96
+
97
+ Inferred layer structure:
98
+ domain ← (no internal dependencies)
99
+ usecase → domain
100
+ external: anyhow
101
+ infrastructure → domain
102
+ external: serde, tokio
103
+
104
+ Generated 'mille.toml'
105
+ ```
106
+
107
+ | Flag | Default | Description |
108
+ |---|---|---|
109
+ | `--output <path>` | `mille.toml` | Write config to a custom path |
110
+ | `--force` | false | Overwrite an existing file without prompting |
111
+ | `--depth <N>` | auto | Layer detection depth from project root |
112
+
113
+ **`--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.
114
+
115
+ 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.
116
+
117
+ ### 2. (Or) Create `mille.toml` manually
87
118
 
88
119
  Place `mille.toml` in your project root:
89
120
 
90
- **Rust project example:**
121
+ **Rust:**
91
122
 
92
123
  ```toml
93
124
  [project]
@@ -132,17 +163,16 @@ external_allow = ["clap"]
132
163
  allow_methods = ["new", "build", "create", "init", "setup"]
133
164
  ```
134
165
 
135
- **Python project example:**
166
+ **TypeScript / JavaScript:**
136
167
 
137
168
  ```toml
138
169
  [project]
139
- name = "my-python-app"
170
+ name = "my-ts-app"
140
171
  root = "."
141
- languages = ["python"]
172
+ languages = ["typescript"]
142
173
 
143
- [resolve.python]
144
- src_root = "."
145
- package_names = ["domain", "usecase", "infrastructure"]
174
+ [resolve.typescript]
175
+ tsconfig = "./tsconfig.json"
146
176
 
147
177
  [[layers]]
148
178
  name = "domain"
@@ -157,8 +187,8 @@ name = "usecase"
157
187
  paths = ["usecase/**"]
158
188
  dependency_mode = "opt-in"
159
189
  allow = ["domain"]
160
- external_mode = "opt-out"
161
- external_deny = []
190
+ external_mode = "opt-in"
191
+ external_allow = ["zod"]
162
192
 
163
193
  [[layers]]
164
194
  name = "infrastructure"
@@ -169,78 +199,79 @@ external_mode = "opt-out"
169
199
  external_deny = []
170
200
  ```
171
201
 
172
- **TypeScript / JavaScript project example:**
202
+ > Use `languages = ["javascript"]` for plain `.js` / `.jsx` projects (no `[resolve.typescript]` needed).
203
+
204
+ **Go:**
173
205
 
174
206
  ```toml
175
207
  [project]
176
- name = "my-ts-app"
208
+ name = "my-go-app"
177
209
  root = "."
178
- languages = ["typescript"]
210
+ languages = ["go"]
179
211
 
180
- [resolve.typescript]
181
- tsconfig = "./tsconfig.json"
212
+ [resolve.go]
213
+ module_name = "github.com/myorg/my-go-app"
182
214
 
183
215
  [[layers]]
184
216
  name = "domain"
185
217
  paths = ["domain/**"]
186
218
  dependency_mode = "opt-in"
187
219
  allow = []
188
- external_mode = "opt-out"
189
- external_deny = []
190
220
 
191
221
  [[layers]]
192
222
  name = "usecase"
193
223
  paths = ["usecase/**"]
194
224
  dependency_mode = "opt-in"
195
225
  allow = ["domain"]
196
- external_mode = "opt-in"
197
- external_allow = ["zod"]
198
226
 
199
227
  [[layers]]
200
228
  name = "infrastructure"
201
229
  paths = ["infrastructure/**"]
202
230
  dependency_mode = "opt-out"
203
231
  deny = []
204
- external_mode = "opt-out"
205
- external_deny = []
206
- ```
207
232
 
208
- > Use `languages = ["javascript"]` for plain `.js` / `.jsx` projects (no `[resolve.typescript]` needed).
233
+ [[layers]]
234
+ name = "cmd"
235
+ paths = ["cmd/**"]
236
+ dependency_mode = "opt-in"
237
+ allow = ["domain", "usecase", "infrastructure"]
238
+ ```
209
239
 
210
- **Go project example:**
240
+ **Python:**
211
241
 
212
242
  ```toml
213
243
  [project]
214
- name = "my-go-app"
244
+ name = "my-python-app"
215
245
  root = "."
216
- languages = ["go"]
246
+ languages = ["python"]
217
247
 
218
- [resolve.go]
219
- module_name = "github.com/myorg/my-go-app"
248
+ [resolve.python]
249
+ src_root = "."
250
+ package_names = ["domain", "usecase", "infrastructure"]
220
251
 
221
252
  [[layers]]
222
253
  name = "domain"
223
254
  paths = ["domain/**"]
224
255
  dependency_mode = "opt-in"
225
256
  allow = []
257
+ external_mode = "opt-out"
258
+ external_deny = []
226
259
 
227
260
  [[layers]]
228
261
  name = "usecase"
229
262
  paths = ["usecase/**"]
230
263
  dependency_mode = "opt-in"
231
264
  allow = ["domain"]
265
+ external_mode = "opt-out"
266
+ external_deny = []
232
267
 
233
268
  [[layers]]
234
269
  name = "infrastructure"
235
270
  paths = ["infrastructure/**"]
236
271
  dependency_mode = "opt-out"
237
272
  deny = []
238
-
239
- [[layers]]
240
- name = "cmd"
241
- paths = ["cmd/**"]
242
- dependency_mode = "opt-in"
243
- allow = ["domain", "usecase", "infrastructure"]
273
+ external_mode = "opt-out"
274
+ external_deny = []
244
275
  ```
245
276
 
246
277
  ### 2. Run `mille check`
@@ -249,12 +280,20 @@ allow = ["domain", "usecase", "infrastructure"]
249
280
  mille check
250
281
  ```
251
282
 
283
+ Output formats:
284
+
285
+ ```sh
286
+ mille check # human-readable terminal output (default)
287
+ mille check --format github-actions # GitHub Actions annotations (::error file=...)
288
+ mille check --format json # machine-readable JSON
289
+ ```
290
+
252
291
  Exit codes:
253
292
 
254
293
  | Code | Meaning |
255
294
  |---|---|
256
295
  | `0` | No violations |
257
- | `1` | One or more errors detected |
296
+ | `1` | One or more violations detected |
258
297
  | `3` | Configuration file error |
259
298
 
260
299
  ## Configuration Reference
@@ -265,100 +304,90 @@ Exit codes:
265
304
  |---|---|
266
305
  | `name` | Project name |
267
306
  | `root` | Root directory for analysis |
268
- | `languages` | List of languages to check (e.g. `["rust", "go"]`) |
307
+ | `languages` | Languages to check: `"rust"`, `"go"`, `"typescript"`, `"javascript"`, `"python"` |
269
308
 
270
309
  ### `[[layers]]`
271
310
 
272
311
  | Key | Description |
273
312
  |---|---|
274
313
  | `name` | Layer name |
275
- | `paths` | Glob patterns for files belonging to this layer |
314
+ | `paths` | Glob patterns for files in this layer |
276
315
  | `dependency_mode` | `"opt-in"` (deny all except `allow`) or `"opt-out"` (allow all except `deny`) |
277
- | `allow` | Layers allowed as dependencies (when `dependency_mode = "opt-in"`) |
278
- | `deny` | Layers forbidden as dependencies (when `dependency_mode = "opt-out"`) |
316
+ | `allow` | Allowed layers (when `dependency_mode = "opt-in"`) |
317
+ | `deny` | Forbidden layers (when `dependency_mode = "opt-out"`) |
279
318
  | `external_mode` | `"opt-in"` or `"opt-out"` for external library usage |
280
- | `external_allow` | Regex patterns of allowed external packages (when `external_mode = "opt-in"`) |
281
- | `external_deny` | Regex patterns of forbidden external packages (when `external_mode = "opt-out"`) |
319
+ | `external_allow` | Allowed external packages (when `external_mode = "opt-in"`) |
320
+ | `external_deny` | Forbidden external packages (when `external_mode = "opt-out"`) |
282
321
 
283
322
  ### `[[layers.allow_call_patterns]]`
284
323
 
285
- Restricts which methods may be called on a given layer's types. Only valid on the `main` layer.
324
+ Restricts which methods may be called on a given layer's types. Only valid on the `main` layer (or equivalent DI entrypoint).
286
325
 
287
326
  | Key | Description |
288
327
  |---|---|
289
328
  | `callee_layer` | The layer whose methods are being restricted |
290
329
  | `allow_methods` | List of method names that are permitted |
291
330
 
331
+ ### `[ignore]`
332
+
333
+ Exclude files from the architecture check entirely, or suppress violations for test/mock files.
334
+
335
+ | Key | Description |
336
+ |---|---|
337
+ | `paths` | Glob patterns — matching files are excluded from collection and not counted in layer stats |
338
+ | `test_patterns` | Glob patterns — matching files are still counted in layer stats but their imports are not violation-checked |
339
+
340
+ ```toml
341
+ [ignore]
342
+ paths = ["**/mock/**", "**/generated/**", "**/testdata/**"]
343
+ test_patterns = ["**/*_test.go", "**/*.spec.ts", "**/*.test.ts"]
344
+ ```
345
+
346
+ **When to use `paths` vs `test_patterns`:**
347
+
348
+ - `paths`: Files that should not be analyzed at all (generated code, vendor directories, mocks)
349
+ - `test_patterns`: Test files that intentionally import across layers (e.g., integration tests that import both domain and infrastructure)
350
+
292
351
  ### `[resolve.typescript]`
293
352
 
294
353
  | Key | Description |
295
354
  |---|---|
296
- | `tsconfig` | Path to `tsconfig.json`. When specified, mille reads `compilerOptions.paths` and resolves path aliases (e.g. `@/*`) as internal imports. |
355
+ | `tsconfig` | Path to `tsconfig.json`. mille reads `compilerOptions.paths` and resolves path aliases (e.g. `@/*`) as internal imports. |
297
356
 
298
357
  **How TypeScript / JavaScript imports are classified:**
299
358
 
300
359
  | Import | Classification |
301
360
  |---|---|
302
- | `import X from "./module"` (starts with `./`) | Internal |
303
- | `import X from "../module"` (starts with `../`) | Internal |
361
+ | `import X from "./module"` | Internal |
362
+ | `import X from "../module"` | Internal |
304
363
  | `import X from "@/module"` (path alias in `tsconfig.json`) | Internal |
305
- | `import X from "react"` (npm package) | External |
306
- | `import fs from "node:fs"` (Node.js built-in) | External |
307
-
308
- For relative imports, mille resolves the path from the importing file and matches it against layer glob patterns. For example, `import { User } from "../domain/user"` in `usecase/user_usecase.ts` resolves to `domain/user`, matching the layer glob `domain/**`.
309
-
310
- For path aliases, mille expands the alias using `compilerOptions.paths` and treats the result as an internal import. For example, with `"@/*": ["./src/*"]`, `import { User } from "@/domain/user"` resolves to `src/domain/user`.
364
+ | `import X from "react"` | External |
365
+ | `import fs from "node:fs"` | External |
311
366
 
312
367
  ### `[resolve.go]`
313
368
 
314
369
  | Key | Description |
315
370
  |---|---|
316
- | `module_name` | Go module name (matches the module path in `go.mod`) |
371
+ | `module_name` | Go module name (matches `go.mod`) |
317
372
 
318
373
  ### `[resolve.python]`
319
374
 
320
375
  | Key | Description |
321
376
  |---|---|
322
377
  | `src_root` | Root directory of the Python source tree (relative to `mille.toml`) |
323
- | `package_names` | List of your own package names (used to classify absolute imports as internal). e.g. `["domain", "usecase", "infrastructure"]` |
378
+ | `package_names` | Your package names imports starting with these are classified as internal. e.g. `["domain", "usecase"]` |
324
379
 
325
380
  **How Python imports are classified:**
326
381
 
327
382
  | Import | Classification |
328
383
  |---|---|
329
384
  | `from .sibling import X` (relative) | Internal |
330
- | `import domain.entity` (matches a `package_names` entry) | Internal |
331
- | `import os`, `import sqlalchemy` (others) | External |
332
-
333
- ## Python API
334
-
335
- In addition to the CLI, the Python package exposes a programmatic API:
336
-
337
- ```python
338
- import mille
339
-
340
- # Run architecture check and get a result object
341
- result = mille.check("path/to/mille.toml") # defaults to "mille.toml"
342
-
343
- print(f"violations: {len(result.violations)}")
344
- for v in result.violations:
345
- print(f" {v.file}:{v.line} {v.from_layer} -> {v.to_layer} ({v.import_path})")
346
-
347
- for stat in result.layer_stats:
348
- print(f" {stat.name}: {stat.file_count} file(s), {stat.violation_count} violation(s)")
349
- ```
350
-
351
- **Types exposed:**
352
-
353
- | Class | Attributes |
354
- |---|---|
355
- | `CheckResult` | `violations: list[Violation]`, `layer_stats: list[LayerStat]` |
356
- | `Violation` | `file`, `line`, `from_layer`, `to_layer`, `import_path`, `kind` |
357
- | `LayerStat` | `name`, `file_count`, `violation_count` |
385
+ | `import domain.entity` (matches `package_names`) | Internal |
386
+ | `import os`, `import sqlalchemy` | External |
358
387
 
359
388
  ## How it Works
360
389
 
361
- mille uses [tree-sitter](https://tree-sitter.github.io/) for AST-based import extraction — no regex heuristics. The core engine is language-agnostic; language-specific logic is isolated to the `parser` and `resolver` layers.
390
+ mille uses [tree-sitter](https://tree-sitter.github.io/) for AST-based import extraction — no regex heuristics.
362
391
 
363
392
  ```
364
393
  mille.toml
@@ -366,7 +395,7 @@ mille.toml
366
395
 
367
396
  Layer definitions
368
397
 
369
- Source files (*.rs, *.go, *.py, ...)
398
+ Source files (*.rs, *.go, *.py, *.ts, *.js, ...)
370
399
  │ tree-sitter parse
371
400
 
372
401
  RawImport list
package/index.js CHANGED
@@ -8,6 +8,16 @@ process.on('warning', (w) => {
8
8
  process.stderr.write(w.stack + '\n');
9
9
  });
10
10
 
11
+ // NOTE: Intercept --version/-V before forwarding to WASM so that the npm
12
+ // package version (updated by `npm version X.Y.Z` at release time) is shown,
13
+ // rather than the version baked into mille.wasm at WASM-build time.
14
+ const userArgs = process.argv.slice(2);
15
+ if (userArgs.includes('--version') || userArgs.includes('-V')) {
16
+ const { version } = require('./package.json');
17
+ process.stdout.write(`mille ${version}\n`);
18
+ process.exit(0);
19
+ }
20
+
11
21
  const { WASI } = require('node:wasi');
12
22
  const { readFileSync } = require('node:fs');
13
23
  const { join } = require('node:path');
package/mille.wasm CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makinzm/mille",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Architecture Checker — Rust-based multi-language architecture linter",
5
5
  "main": "index.js",
6
6
  "bin": {