archprint 0.1.0
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.
- package/CHANGELOG.md +4 -0
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/cli/codeframe.d.ts +6 -0
- package/dist/cli/codeframe.d.ts.map +1 -0
- package/dist/cli/codeframe.js +22 -0
- package/dist/cli/codeframe.js.map +1 -0
- package/dist/cli/generate.d.ts +37 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +259 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/cli/init.d.ts +21 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +19 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/outputs-manifest.d.ts +10 -0
- package/dist/cli/outputs-manifest.d.ts.map +1 -0
- package/dist/cli/outputs-manifest.js +53 -0
- package/dist/cli/outputs-manifest.js.map +1 -0
- package/dist/cli/program.d.ts +4 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +316 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/recommend.d.ts +18 -0
- package/dist/cli/recommend.d.ts.map +1 -0
- package/dist/cli/recommend.js +193 -0
- package/dist/cli/recommend.js.map +1 -0
- package/dist/cli/report.d.ts +8 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +482 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli/rule-guidance.d.ts +6 -0
- package/dist/cli/rule-guidance.d.ts.map +1 -0
- package/dist/cli/rule-guidance.js +22 -0
- package/dist/cli/rule-guidance.js.map +1 -0
- package/dist/cli/scan.d.ts +54 -0
- package/dist/cli/scan.d.ts.map +1 -0
- package/dist/cli/scan.js +103 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli/summary.d.ts +18 -0
- package/dist/cli/summary.d.ts.map +1 -0
- package/dist/cli/summary.js +59 -0
- package/dist/cli/summary.js.map +1 -0
- package/dist/cli/wiring.d.ts +42 -0
- package/dist/cli/wiring.d.ts.map +1 -0
- package/dist/cli/wiring.js +246 -0
- package/dist/cli/wiring.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -0
- package/dist/data/adoption-catalog.d.ts +16 -0
- package/dist/data/adoption-catalog.d.ts.map +1 -0
- package/dist/data/adoption-catalog.js +254 -0
- package/dist/data/adoption-catalog.js.map +1 -0
- package/dist/detector/app-isolation-detector.d.ts +22 -0
- package/dist/detector/app-isolation-detector.d.ts.map +1 -0
- package/dist/detector/app-isolation-detector.js +17 -0
- package/dist/detector/app-isolation-detector.js.map +1 -0
- package/dist/detector/confidence-gate.d.ts +33 -0
- package/dist/detector/confidence-gate.d.ts.map +1 -0
- package/dist/detector/confidence-gate.js +62 -0
- package/dist/detector/confidence-gate.js.map +1 -0
- package/dist/detector/console-isolation-detector.d.ts +17 -0
- package/dist/detector/console-isolation-detector.d.ts.map +1 -0
- package/dist/detector/console-isolation-detector.js +23 -0
- package/dist/detector/console-isolation-detector.js.map +1 -0
- package/dist/detector/cycle-detector.d.ts +18 -0
- package/dist/detector/cycle-detector.d.ts.map +1 -0
- package/dist/detector/cycle-detector.js +34 -0
- package/dist/detector/cycle-detector.js.map +1 -0
- package/dist/detector/deep-relative-detector.d.ts +17 -0
- package/dist/detector/deep-relative-detector.d.ts.map +1 -0
- package/dist/detector/deep-relative-detector.js +43 -0
- package/dist/detector/deep-relative-detector.js.map +1 -0
- package/dist/detector/dependency-internals-detector.d.ts +17 -0
- package/dist/detector/dependency-internals-detector.d.ts.map +1 -0
- package/dist/detector/dependency-internals-detector.js +65 -0
- package/dist/detector/dependency-internals-detector.js.map +1 -0
- package/dist/detector/entry-purity-detector.d.ts +19 -0
- package/dist/detector/entry-purity-detector.d.ts.map +1 -0
- package/dist/detector/entry-purity-detector.js +32 -0
- package/dist/detector/entry-purity-detector.js.map +1 -0
- package/dist/detector/env-access-detector.d.ts +17 -0
- package/dist/detector/env-access-detector.d.ts.map +1 -0
- package/dist/detector/env-access-detector.js +25 -0
- package/dist/detector/env-access-detector.js.map +1 -0
- package/dist/detector/family-maturity.d.ts +3 -0
- package/dist/detector/family-maturity.d.ts.map +1 -0
- package/dist/detector/family-maturity.js +12 -0
- package/dist/detector/family-maturity.js.map +1 -0
- package/dist/detector/feature-slice-detector.d.ts +22 -0
- package/dist/detector/feature-slice-detector.d.ts.map +1 -0
- package/dist/detector/feature-slice-detector.js +17 -0
- package/dist/detector/feature-slice-detector.js.map +1 -0
- package/dist/detector/layer-detector.d.ts +37 -0
- package/dist/detector/layer-detector.d.ts.map +1 -0
- package/dist/detector/layer-detector.js +165 -0
- package/dist/detector/layer-detector.js.map +1 -0
- package/dist/detector/marker-inference.d.ts +21 -0
- package/dist/detector/marker-inference.d.ts.map +1 -0
- package/dist/detector/marker-inference.js +236 -0
- package/dist/detector/marker-inference.js.map +1 -0
- package/dist/detector/orphan-detector.d.ts +12 -0
- package/dist/detector/orphan-detector.d.ts.map +1 -0
- package/dist/detector/orphan-detector.js +24 -0
- package/dist/detector/orphan-detector.js.map +1 -0
- package/dist/detector/pattern-detector.d.ts +48 -0
- package/dist/detector/pattern-detector.d.ts.map +1 -0
- package/dist/detector/pattern-detector.js +139 -0
- package/dist/detector/pattern-detector.js.map +1 -0
- package/dist/detector/phantom-dependency-detector.d.ts +18 -0
- package/dist/detector/phantom-dependency-detector.d.ts.map +1 -0
- package/dist/detector/phantom-dependency-detector.js +91 -0
- package/dist/detector/phantom-dependency-detector.js.map +1 -0
- package/dist/detector/public-api-detector.d.ts +23 -0
- package/dist/detector/public-api-detector.d.ts.map +1 -0
- package/dist/detector/public-api-detector.js +87 -0
- package/dist/detector/public-api-detector.js.map +1 -0
- package/dist/detector/reachability.d.ts +13 -0
- package/dist/detector/reachability.d.ts.map +1 -0
- package/dist/detector/reachability.js +64 -0
- package/dist/detector/reachability.js.map +1 -0
- package/dist/detector/role-layering-detector.d.ts +28 -0
- package/dist/detector/role-layering-detector.d.ts.map +1 -0
- package/dist/detector/role-layering-detector.js +75 -0
- package/dist/detector/role-layering-detector.js.map +1 -0
- package/dist/detector/self-consistency.d.ts +7 -0
- package/dist/detector/self-consistency.d.ts.map +1 -0
- package/dist/detector/self-consistency.js +65 -0
- package/dist/detector/self-consistency.js.map +1 -0
- package/dist/detector/server-client-detector.d.ts +19 -0
- package/dist/detector/server-client-detector.d.ts.map +1 -0
- package/dist/detector/server-client-detector.js +51 -0
- package/dist/detector/server-client-detector.js.map +1 -0
- package/dist/detector/sibling-isolation.d.ts +23 -0
- package/dist/detector/sibling-isolation.d.ts.map +1 -0
- package/dist/detector/sibling-isolation.js +76 -0
- package/dist/detector/sibling-isolation.js.map +1 -0
- package/dist/detector/stories-isolation-detector.d.ts +19 -0
- package/dist/detector/stories-isolation-detector.d.ts.map +1 -0
- package/dist/detector/stories-isolation-detector.js +31 -0
- package/dist/detector/stories-isolation-detector.js.map +1 -0
- package/dist/detector/test-isolation-detector.d.ts +18 -0
- package/dist/detector/test-isolation-detector.d.ts.map +1 -0
- package/dist/detector/test-isolation-detector.js +34 -0
- package/dist/detector/test-isolation-detector.js.map +1 -0
- package/dist/detector/ui-data-isolation-detector.d.ts +19 -0
- package/dist/detector/ui-data-isolation-detector.d.ts.map +1 -0
- package/dist/detector/ui-data-isolation-detector.js +41 -0
- package/dist/detector/ui-data-isolation-detector.js.map +1 -0
- package/dist/detector/workspace-package-detector.d.ts +19 -0
- package/dist/detector/workspace-package-detector.d.ts.map +1 -0
- package/dist/detector/workspace-package-detector.js +65 -0
- package/dist/detector/workspace-package-detector.js.map +1 -0
- package/dist/generator/app-isolation-emitters.d.ts +19 -0
- package/dist/generator/app-isolation-emitters.d.ts.map +1 -0
- package/dist/generator/app-isolation-emitters.js +19 -0
- package/dist/generator/app-isolation-emitters.js.map +1 -0
- package/dist/generator/console-isolation-emitters.d.ts +8 -0
- package/dist/generator/console-isolation-emitters.d.ts.map +1 -0
- package/dist/generator/console-isolation-emitters.js +10 -0
- package/dist/generator/console-isolation-emitters.js.map +1 -0
- package/dist/generator/deep-relative-emitters.d.ts +14 -0
- package/dist/generator/deep-relative-emitters.d.ts.map +1 -0
- package/dist/generator/deep-relative-emitters.js +20 -0
- package/dist/generator/deep-relative-emitters.js.map +1 -0
- package/dist/generator/dependency-internals-emitters.d.ts +17 -0
- package/dist/generator/dependency-internals-emitters.d.ts.map +1 -0
- package/dist/generator/dependency-internals-emitters.js +20 -0
- package/dist/generator/dependency-internals-emitters.js.map +1 -0
- package/dist/generator/entry-purity-emitters.d.ts +17 -0
- package/dist/generator/entry-purity-emitters.d.ts.map +1 -0
- package/dist/generator/entry-purity-emitters.js +24 -0
- package/dist/generator/entry-purity-emitters.js.map +1 -0
- package/dist/generator/env-access-emitters.d.ts +4 -0
- package/dist/generator/env-access-emitters.d.ts.map +1 -0
- package/dist/generator/env-access-emitters.js +19 -0
- package/dist/generator/env-access-emitters.js.map +1 -0
- package/dist/generator/eslint-plugin-emitter.d.ts +14 -0
- package/dist/generator/eslint-plugin-emitter.d.ts.map +1 -0
- package/dist/generator/eslint-plugin-emitter.js +57 -0
- package/dist/generator/eslint-plugin-emitter.js.map +1 -0
- package/dist/generator/feature-slice-emitters.d.ts +19 -0
- package/dist/generator/feature-slice-emitters.d.ts.map +1 -0
- package/dist/generator/feature-slice-emitters.js +19 -0
- package/dist/generator/feature-slice-emitters.js.map +1 -0
- package/dist/generator/graph-emitters.d.ts +11 -0
- package/dist/generator/graph-emitters.d.ts.map +1 -0
- package/dist/generator/graph-emitters.js +56 -0
- package/dist/generator/graph-emitters.js.map +1 -0
- package/dist/generator/layer-emitters.d.ts +39 -0
- package/dist/generator/layer-emitters.d.ts.map +1 -0
- package/dist/generator/layer-emitters.js +43 -0
- package/dist/generator/layer-emitters.js.map +1 -0
- package/dist/generator/phantom-dependency-emitters.d.ts +17 -0
- package/dist/generator/phantom-dependency-emitters.d.ts.map +1 -0
- package/dist/generator/phantom-dependency-emitters.js +19 -0
- package/dist/generator/phantom-dependency-emitters.js.map +1 -0
- package/dist/generator/public-api-emitters.d.ts +19 -0
- package/dist/generator/public-api-emitters.d.ts.map +1 -0
- package/dist/generator/public-api-emitters.js +19 -0
- package/dist/generator/public-api-emitters.js.map +1 -0
- package/dist/generator/role-layering-emitters.d.ts +18 -0
- package/dist/generator/role-layering-emitters.d.ts.map +1 -0
- package/dist/generator/role-layering-emitters.js +19 -0
- package/dist/generator/role-layering-emitters.js.map +1 -0
- package/dist/generator/rule-generator.d.ts +14 -0
- package/dist/generator/rule-generator.d.ts.map +1 -0
- package/dist/generator/rule-generator.js +174 -0
- package/dist/generator/rule-generator.js.map +1 -0
- package/dist/generator/server-client-emitters.d.ts +17 -0
- package/dist/generator/server-client-emitters.d.ts.map +1 -0
- package/dist/generator/server-client-emitters.js +18 -0
- package/dist/generator/server-client-emitters.js.map +1 -0
- package/dist/generator/stories-isolation-emitters.d.ts +17 -0
- package/dist/generator/stories-isolation-emitters.d.ts.map +1 -0
- package/dist/generator/stories-isolation-emitters.js +19 -0
- package/dist/generator/stories-isolation-emitters.js.map +1 -0
- package/dist/generator/test-isolation-emitters.d.ts +17 -0
- package/dist/generator/test-isolation-emitters.d.ts.map +1 -0
- package/dist/generator/test-isolation-emitters.js +19 -0
- package/dist/generator/test-isolation-emitters.js.map +1 -0
- package/dist/generator/ui-data-isolation-emitters.d.ts +17 -0
- package/dist/generator/ui-data-isolation-emitters.d.ts.map +1 -0
- package/dist/generator/ui-data-isolation-emitters.js +25 -0
- package/dist/generator/ui-data-isolation-emitters.js.map +1 -0
- package/dist/generator/workspace-package-emitters.d.ts +4 -0
- package/dist/generator/workspace-package-emitters.d.ts.map +1 -0
- package/dist/generator/workspace-package-emitters.js +24 -0
- package/dist/generator/workspace-package-emitters.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner/app-dirs.d.ts +2 -0
- package/dist/scanner/app-dirs.d.ts.map +1 -0
- package/dist/scanner/app-dirs.js +58 -0
- package/dist/scanner/app-dirs.js.map +1 -0
- package/dist/scanner/barrel-resolver.d.ts +4 -0
- package/dist/scanner/barrel-resolver.d.ts.map +1 -0
- package/dist/scanner/barrel-resolver.js +40 -0
- package/dist/scanner/barrel-resolver.js.map +1 -0
- package/dist/scanner/file-walker.d.ts +21 -0
- package/dist/scanner/file-walker.d.ts.map +1 -0
- package/dist/scanner/file-walker.js +279 -0
- package/dist/scanner/file-walker.js.map +1 -0
- package/dist/scanner/ignore-filter.d.ts +3 -0
- package/dist/scanner/ignore-filter.d.ts.map +1 -0
- package/dist/scanner/ignore-filter.js +26 -0
- package/dist/scanner/ignore-filter.js.map +1 -0
- package/dist/scanner/import-graph.d.ts +13 -0
- package/dist/scanner/import-graph.d.ts.map +1 -0
- package/dist/scanner/import-graph.js +103 -0
- package/dist/scanner/import-graph.js.map +1 -0
- package/dist/scanner/resolve-import.d.ts +6 -0
- package/dist/scanner/resolve-import.d.ts.map +1 -0
- package/dist/scanner/resolve-import.js +42 -0
- package/dist/scanner/resolve-import.js.map +1 -0
- package/dist/scanner/role-classifier.d.ts +12 -0
- package/dist/scanner/role-classifier.d.ts.map +1 -0
- package/dist/scanner/role-classifier.js +158 -0
- package/dist/scanner/role-classifier.js.map +1 -0
- package/dist/scanner/usage-scanner.d.ts +12 -0
- package/dist/scanner/usage-scanner.d.ts.map +1 -0
- package/dist/scanner/usage-scanner.js +31 -0
- package/dist/scanner/usage-scanner.js.map +1 -0
- package/dist/scanner/workspace-packages.d.ts +3 -0
- package/dist/scanner/workspace-packages.d.ts.map +1 -0
- package/dist/scanner/workspace-packages.js +96 -0
- package/dist/scanner/workspace-packages.js.map +1 -0
- package/dist/scanner/workspace-resolver.d.ts +2 -0
- package/dist/scanner/workspace-resolver.d.ts.map +1 -0
- package/dist/scanner/workspace-resolver.js +57 -0
- package/dist/scanner/workspace-resolver.js.map +1 -0
- package/package.json +100 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oluwatomiwa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Archprint
|
|
2
|
+
|
|
3
|
+
**Mine the architecture rules your repo already enforces, with the evidence attached.**
|
|
4
|
+
|
|
5
|
+
Archprint scans a TypeScript repository's real import graph, finds the architectural boundaries the code
|
|
6
|
+
already respects, and turns the ones that pass a statistical confidence gate into deterministic, ready to
|
|
7
|
+
install lint rules. Every rule ships with the evidence behind it: how many files conform, how many break it,
|
|
8
|
+
and how confident the inference is.
|
|
9
|
+
|
|
10
|
+
Your `CLAUDE.md` is guidance. Your lint rules are enforcement. Archprint closes the gap by generating the
|
|
11
|
+
enforcement from patterns your codebase already demonstrates, so you adopt rules you can trust instead of
|
|
12
|
+
authoring them by hand.
|
|
13
|
+
|
|
14
|
+
**What auto-enforces vs. what you review.** Archprint is honest about which of its inferences it will stand
|
|
15
|
+
behind unattended. An adversarial correctness audit (three rounds over four real repositories) found that the
|
|
16
|
+
_mechanical_ families, ones grounded in unambiguous signals (no cycles, production must not import tests, no
|
|
17
|
+
`console` in library code, no undeclared dependencies, deep-relative import style, public-API barrels, no
|
|
18
|
+
reaching into a dependency's internals, and the DB/UI-in-server-entry rule), had zero false positives every
|
|
19
|
+
round. So those auto-generate as enforcement. The _structural-inference_ families (layer and role boundaries,
|
|
20
|
+
UI/data separation, entry purity, server/client, feature-slice and app isolation) infer a "layer" or "role"
|
|
21
|
+
from paths, which can be wrong, so Archprint holds them for human review by default rather than silently
|
|
22
|
+
enforcing them. Nothing whose inferred layer or role could be wrong is written as enforcement without you
|
|
23
|
+
opting in.
|
|
24
|
+
|
|
25
|
+
> Status: `0.1.0`, pre-stable (0.x may break between minor versions). Not yet published to npm. Production-ready
|
|
26
|
+
> today: the insight commands (`scan`, `recommend`) and the auto-enforcement of the mechanical families above.
|
|
27
|
+
> The structural families are review-only while they are hardened.
|
|
28
|
+
|
|
29
|
+
## What makes it different
|
|
30
|
+
|
|
31
|
+
Established TypeScript tools (dependency-cruiser, eslint-plugin-boundaries, Nx, Sheriff, ts-arch) all
|
|
32
|
+
**enforce** architecture rules you write by hand. Archprint **infers** them from the actual import graph and
|
|
33
|
+
**gates each one on statistical evidence** before proposing it. Across the TypeScript ecosystem, no other tool
|
|
34
|
+
does either (see the comparison below). It then emits into those existing tools' formats, so it complements
|
|
35
|
+
your stack rather than replacing it.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Not yet on npm. For now, build from source:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/Tommkruix/archprint
|
|
43
|
+
cd archprint
|
|
44
|
+
npm ci
|
|
45
|
+
npm run build
|
|
46
|
+
node dist/cli.js scan <path-to-your-app>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Once published, it will install as a normal dev dependency:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install --save-dev archprint
|
|
53
|
+
npx archprint scan .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Requires Node >= 20. Point Archprint at an app directory that has a `tsconfig.json` (for a monorepo, a
|
|
57
|
+
package such as `apps/web`; a monorepo root is fine too, Archprint discovers the app directories).
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# One-shot setup: detect the stack, enforce the rules your code already follows,
|
|
63
|
+
# and record what to adopt next in archprint.json
|
|
64
|
+
archprint init apps/web
|
|
65
|
+
|
|
66
|
+
# See the rules your repo already follows, with the evidence
|
|
67
|
+
archprint scan apps/web
|
|
68
|
+
|
|
69
|
+
# Write the auto-trusted (mechanical) rules to disk (rule files + tool configs).
|
|
70
|
+
# Structural-inference rules are held for review; add --include-structural to emit them too.
|
|
71
|
+
archprint generate apps/web --out archprint-rules
|
|
72
|
+
|
|
73
|
+
# Inspect the gate evidence behind one rule
|
|
74
|
+
archprint explain AP-002 apps/web
|
|
75
|
+
|
|
76
|
+
# Generate a single rule by id after reviewing it (including a SUGGEST rule)
|
|
77
|
+
archprint generate apps/web --rule AP-001
|
|
78
|
+
|
|
79
|
+
# Recommend a rule set from the evidence and the detected stack (fresh repos too)
|
|
80
|
+
archprint recommend apps/web
|
|
81
|
+
|
|
82
|
+
# Reference the generated rules from the enforcement tools your repo uses (managed, reversible)
|
|
83
|
+
archprint wire
|
|
84
|
+
|
|
85
|
+
# Remove archprint's files and any wired references (clean uninstall)
|
|
86
|
+
archprint eject
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Re-running `generate` (or `init`) refreshes the files in `archprint-rules/` and removes any rule the
|
|
90
|
+
evidence no longer supports, so the output never drifts from the current codebase. `wire` detects the
|
|
91
|
+
enforcement tools your repo already uses (a flat eslint config, a `.dependency-cruiser.json`) and inserts a
|
|
92
|
+
single managed reference into each, one that survives those regenerations; `eject` removes archprint's files
|
|
93
|
+
and every wired reference, restoring each config exactly. For a tool config it cannot safely edit (a JS
|
|
94
|
+
dependency-cruiser config, say), it prints the exact snippet to paste. The flagship forbidden-import rules
|
|
95
|
+
(AP-) ship as a generated local eslint plugin that the eslint reference activates, so wiring the eslint config
|
|
96
|
+
enforces them too, no extra install.
|
|
97
|
+
|
|
98
|
+
`recommend` sorts every rule family into three tiers: rules your code already
|
|
99
|
+
follows (enforce now), rules with thin evidence (review and adopt), and rules that
|
|
100
|
+
comparable repos commonly follow but yours does not yet (adopt from day one). Each
|
|
101
|
+
recommendation carries the evidence behind it: the share of comparable repos (your
|
|
102
|
+
detected stack, else overall) that already enforce that rule, mined from a census of
|
|
103
|
+
tens of thousands of public TypeScript repositories. The "adopt from day one" tier is
|
|
104
|
+
driven by that census rather than hand-picked defaults, so on a fresh repo, where
|
|
105
|
+
there is little code to infer from, it still gives you a stack-aware baseline backed
|
|
106
|
+
by what the ecosystem actually does.
|
|
107
|
+
|
|
108
|
+
## Example
|
|
109
|
+
|
|
110
|
+
A real scan of [inbox-zero](https://github.com/elie222/inbox-zero) (`apps/web`, 2,232 TypeScript files),
|
|
111
|
+
trimmed:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
Archprint v0.1.0
|
|
115
|
+
Scanned 2,232 TypeScript files
|
|
116
|
+
Workspace aliases: 18 resolved
|
|
117
|
+
|
|
118
|
+
GENERATED RULES
|
|
119
|
+
AP-002 no-ui-layer-in-server-entry confidence 97%
|
|
120
|
+
Evidence: 216/217 role files conform (99.5% observed)
|
|
121
|
+
Exceptions: 1
|
|
122
|
+
|
|
123
|
+
LAYER BOUNDARIES (review before enforcing)
|
|
124
|
+
utils !-> app layer boundary confidence 99%
|
|
125
|
+
Evidence: 650/653 utils files conform (99.5%); 451 app file(s) depend on utils
|
|
126
|
+
hooks !-> app layer boundary confidence 94%
|
|
127
|
+
Evidence: 65/65 hooks files conform (100%); 121 app file(s) depend on hooks
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`AP-002` is a mechanical family, so it auto-generates as enforcement. The layer boundaries are inferred, so
|
|
131
|
+
they are shown for review, not written as enforcement unless you pass `--include-structural`.
|
|
132
|
+
|
|
133
|
+
Every number is measured from the import graph, not estimated.
|
|
134
|
+
|
|
135
|
+
## What Archprint detects
|
|
136
|
+
|
|
137
|
+
Ships as: **Auto** = auto-generated as enforcement (mechanical families, 0 false positives across the
|
|
138
|
+
correctness audit). **Review** = held for human review by default; emit with `--include-structural` (the
|
|
139
|
+
inferred layer/role can be wrong, so it is not enforced silently). **Report** = surfaced only, never enforced.
|
|
140
|
+
|
|
141
|
+
| Detector | Rule it can infer | Ships as |
|
|
142
|
+
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
143
|
+
| Forbidden imports (marker based) | A role (route handler, server entry) must not import a target (the DB client, the UI layer) | Auto |
|
|
144
|
+
| Circular dependencies | The module graph should stay acyclic (gated on how cycle free it already is) | Auto |
|
|
145
|
+
| Test isolation | Production (non-test) code must not import test or spec files | Auto |
|
|
146
|
+
| Dependency hygiene | Import third-party packages by their public entry, not a dependency's `src`/`internal` internals | Auto |
|
|
147
|
+
| Dependency declaration | Every imported third-party package must be declared in `package.json` (no phantom/transitive deps) | Auto |
|
|
148
|
+
| Import style | Prefer workspace aliases over deep relative imports (`../../../`) | Auto |
|
|
149
|
+
| Console isolation | Library (non-CLI) code must not call `console.*` | Auto |
|
|
150
|
+
| Public API (barrel) boundaries | Files outside a feature or package must import it through its `index` barrel, not deep import its internals | Auto |
|
|
151
|
+
| Layer boundaries | Files in one layer must not import another, inferred from the dominant dependency direction | Review |
|
|
152
|
+
| Role layering | Semantic tiers keep their direction (a REPOSITORY must not import a SERVICE, a SERVICE must not import a CONTROLLER) | Review |
|
|
153
|
+
| Entry purity | Framework entries (pages, routes, layouts) must not be imported by other first-party code | Review |
|
|
154
|
+
| UI / data separation | Reusable UI components must not import the DB/data layer directly | Review |
|
|
155
|
+
| Server / client boundary | A Next.js `"use client"` module must not import a `server-only` module | Review |
|
|
156
|
+
| Feature-slice isolation | Sibling slices under a `features`/`modules`/`slices`/`domains` container must not import each other | Review |
|
|
157
|
+
| App isolation | Sibling apps under an `apps`/`services` container must not import each other directly | Review |
|
|
158
|
+
| Env access | Read `process.env` only in the config/env layer | Review |
|
|
159
|
+
| Workspace package API | Import a monorepo workspace package by its name, not a deep path into its source | Review |
|
|
160
|
+
| Stories isolation | Storybook `.stories` files must not be imported by other code | Review |
|
|
161
|
+
| Orphan modules | Files nothing imports and that are not framework entries (dead code candidates) | Report |
|
|
162
|
+
| Transitive reachability | A layer boundary that a plain import rule passes but that leaks through an intermediary layer | Report |
|
|
163
|
+
|
|
164
|
+
## The confidence gate
|
|
165
|
+
|
|
166
|
+
Archprint never proposes a rule as enforceable on a thin sample. Each candidate is scored with a **Wilson
|
|
167
|
+
score lower bound** on its true conformance rate, which fuses the observed ratio and the sample size into one
|
|
168
|
+
number, so 5 of 5 clean files is not treated as evidence of a 90% rule but 40 of 40 is.
|
|
169
|
+
|
|
170
|
+
- **AUTO** (enforceable): the 95% lower bound on conformance is at least 90%, with at most 3 exceptions and a
|
|
171
|
+
confidently classified role.
|
|
172
|
+
- **SUGGEST** (provisional): the pattern looks like a rule (at least 80% observed) but the sample is too thin
|
|
173
|
+
to be confident. Surfaced for review, not auto generated.
|
|
174
|
+
- **REJECT**: not enough signal.
|
|
175
|
+
|
|
176
|
+
The statistical gate is necessary but not sufficient: a rule can be statistically clean yet semantically wrong
|
|
177
|
+
if the inferred "layer" or "role" is not real. So a second, evidence-based gate sits on top of it: only the
|
|
178
|
+
_mechanical_ families (see the "Ships as: Auto" rows above), which an adversarial correctness audit found had
|
|
179
|
+
zero false positives across three rounds and four repositories, auto-generate as enforcement. The
|
|
180
|
+
_structural-inference_ families are capped at review regardless of their statistical score until they earn the
|
|
181
|
+
same clean record. The bias is deliberate and conservative: one wrong enforced rule hurts credibility more than
|
|
182
|
+
zero rules.
|
|
183
|
+
|
|
184
|
+
## Output formats
|
|
185
|
+
|
|
186
|
+
`archprint generate` writes into the formats your existing tools already read:
|
|
187
|
+
|
|
188
|
+
- **dependency-cruiser** `forbidden` rulesets: by default the mechanical boundaries (public-API deep-import,
|
|
189
|
+
test-isolation, dependency-internals); the structural ones (layer, role-layering, feature-slice,
|
|
190
|
+
app-isolation, entry-purity) are written only with `--include-structural`, after you review them
|
|
191
|
+
- **eslint-plugin-boundaries** element-types config, and **ESLint core** rules (`no-restricted-imports`) for
|
|
192
|
+
import-style boundaries
|
|
193
|
+
- **ESLint rule files** for marker based patterns: a rule card (`.md`), the rule (`.ts`), and a passing and a
|
|
194
|
+
failing fixture
|
|
195
|
+
- **Mermaid** and **Graphviz DOT** of the layer dependency graph, so the inferred architecture is visible and
|
|
196
|
+
its violations are marked
|
|
197
|
+
|
|
198
|
+
## How it compares
|
|
199
|
+
|
|
200
|
+
Verified against each tool's documentation (TypeScript ecosystem). The two columns that matter are the ones no
|
|
201
|
+
other TypeScript tool fills:
|
|
202
|
+
|
|
203
|
+
| Tool | Enforces arch rules | Auto-infers from the import graph | Attaches statistical evidence |
|
|
204
|
+
| ----------------------------- | :-----------------: | :-------------------------------: | :---------------------------: |
|
|
205
|
+
| **Archprint** | yes | **yes** | **yes** |
|
|
206
|
+
| dependency-cruiser | yes | no | no |
|
|
207
|
+
| eslint-plugin-boundaries | yes | no | no |
|
|
208
|
+
| @nx/enforce-module-boundaries | yes | no | no |
|
|
209
|
+
| Sheriff | yes | no | no |
|
|
210
|
+
| ts-arch | yes | no | no |
|
|
211
|
+
| madge / knip | analysis only | no | no |
|
|
212
|
+
|
|
213
|
+
Honest caveat: in other ecosystems, [Tach](https://github.com/gauge-sh/tach) (Python) and ArchLint (Java) do
|
|
214
|
+
auto-infer module boundaries, so Archprint's specific niche is auto-inference **plus statistical evidence
|
|
215
|
+
gating in the TypeScript ecosystem**. Archprint also overlaps in detection with dependency-cruiser (cycles,
|
|
216
|
+
orphans, reachability) and knip (dead code); rather than compete, it emits into those tools' formats.
|
|
217
|
+
|
|
218
|
+
## Commands
|
|
219
|
+
|
|
220
|
+
| Command | What it does |
|
|
221
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
222
|
+
| `archprint init [path]` | Zero-config setup: detect the stack, enforce the rules the code already follows, and write an `archprint.json` with the adopt tiers. `--include-structural`, `--out <dir>`, `--fast`, `--force`. |
|
|
223
|
+
| `archprint scan [path]` | Report the rules the repo already follows, with evidence. `--deep` resolves through barrels and aliases. |
|
|
224
|
+
| `archprint generate [path]` | Write the auto-trusted mechanical rules + tool configs; structural rules held for review. `--rule <id>` emits one reviewed rule (including a SUGGEST rule). `--include-structural`, `--out <dir>`, `--fast`. |
|
|
225
|
+
| `archprint explain <id> [path]` | Show the gate breakdown for one rule, with a codeframe per exception plus how-to-fix, when-not-to-use, and how-to-enforce. |
|
|
226
|
+
| `archprint recommend [path]` | Recommend a rule set from the repo's evidence and detected stack (works on a fresh repo too). |
|
|
227
|
+
| `archprint wire` | Reference the generated rules from the enforcement tools your repo uses (flat eslint config, `.dependency-cruiser.json`) via a managed, reversible reference. `--out <dir>`, `--dry-run`. |
|
|
228
|
+
| `archprint eject` | Remove archprint's generated files, its manifests, and any wired references. `--out <dir>`, `--dry-run`. |
|
|
229
|
+
|
|
230
|
+
## Documentation
|
|
231
|
+
|
|
232
|
+
Full docs live in [`docs/`](./docs/): [getting started](./docs/getting-started.md),
|
|
233
|
+
[concepts](./docs/concepts.md) (the confidence gate, mechanical vs. structural, fast vs. deep, the
|
|
234
|
+
generate/wire/eject lifecycle), and the [rule-family reference](./docs/rules.md) (what each rule detects, how it
|
|
235
|
+
ships, and when not to use it).
|
|
236
|
+
|
|
237
|
+
## Fast and deep modes
|
|
238
|
+
|
|
239
|
+
`scan` defaults to a **fast** specifier level pass (no type checker). `generate` defaults to a
|
|
240
|
+
**deep** pass that resolves through barrels and workspace aliases, since generation is the commitment point.
|
|
241
|
+
Structural analysis (cycles, orphans, reachability, public-API) always uses the fast graph: it is faithful to
|
|
242
|
+
deep resolution for those, and public-API detection in fact requires it (deep resolution would resolve through
|
|
243
|
+
a barrel and erase the barrel-versus-deep signal).
|
|
244
|
+
|
|
245
|
+
## Determinism
|
|
246
|
+
|
|
247
|
+
Same repo plus same version produces the same output. Analysis is pure and sorted; there is no randomness.
|
|
248
|
+
|
|
249
|
+
## Status and roadmap
|
|
250
|
+
|
|
251
|
+
`0.1.0`, pre-stable. The engine (twenty detectors, five output formats, the confidence gate) is in place and
|
|
252
|
+
tested, and an adversarial correctness audit (three rounds, four real repositories) drove the false-positive
|
|
253
|
+
rate on auto-generated rules to zero for the mechanical families, which is why those auto-enforce while the
|
|
254
|
+
structural-inference families are held for review.
|
|
255
|
+
|
|
256
|
+
Production-ready today: `scan` and `recommend` (insight), and auto-enforcement of the mechanical families.
|
|
257
|
+
Still ahead: hardening the structural families toward auto-enforcement (a real per-file role-confidence measure,
|
|
258
|
+
layer-cohesion, role-classifier ordering), a self-consistency check at generate time, a pinned regression
|
|
259
|
+
corpus, an `init` scaffolder for fresh repos, and broader framework role coverage.
|
|
260
|
+
|
|
261
|
+
## Contributing
|
|
262
|
+
|
|
263
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The project lints, type checks, and tests itself; every change keeps
|
|
264
|
+
coverage above its thresholds and ships a changeset.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeframe.d.ts","sourceRoot":"","sources":["../../src/cli/codeframe.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAChB,SAAS,GAAG,IAAI,CAiBlB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function locateImport(appDir, relativeFile, specifier) {
|
|
4
|
+
let source;
|
|
5
|
+
try {
|
|
6
|
+
source = readFileSync(path.join(appDir, relativeFile), 'utf8');
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const single = `'${specifier}'`;
|
|
12
|
+
const double = `"${specifier}"`;
|
|
13
|
+
const lines = source.split(/\r?\n/);
|
|
14
|
+
for (let index = 0; index < lines.length; index++) {
|
|
15
|
+
const text = lines[index];
|
|
16
|
+
if (text.includes(single) || text.includes(double)) {
|
|
17
|
+
return { line: index + 1, text: text.trim() };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=codeframe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codeframe.js","sourceRoot":"","sources":["../../src/cli/codeframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAOlC,MAAM,UAAU,YAAY,CAC1B,MAAc,EACd,YAAoB,EACpB,SAAiB;IAEjB,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,SAAS,GAAG,CAAC;IAChC,MAAM,MAAM,GAAG,IAAI,SAAS,GAAG,CAAC;IAChC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACpC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAClD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACnD,OAAO,EAAE,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAChD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { GenerationStatus } from '../detector/confidence-gate.js';
|
|
2
|
+
import type { ScannedPattern, ScanResult } from './scan.js';
|
|
3
|
+
export declare function writeRules(scan: ScanResult, outDir: string, statuses?: readonly GenerationStatus[]): string[];
|
|
4
|
+
export declare function emitOne(pattern: ScannedPattern, appDir: string, outDir: string): string;
|
|
5
|
+
export declare function writeLayerConfig(scan: ScanResult, outDir: string, statuses?: readonly GenerationStatus[]): string[];
|
|
6
|
+
export declare function writePublicApiConfig(scan: ScanResult, outDir: string, statuses?: readonly GenerationStatus[]): string[];
|
|
7
|
+
export declare function writeFeatureSliceConfig(scan: ScanResult, outDir: string, statuses?: readonly GenerationStatus[]): string[];
|
|
8
|
+
export declare function writeAppIsolationConfig(scan: ScanResult, outDir: string, statuses?: readonly GenerationStatus[]): string[];
|
|
9
|
+
export declare function writeTestIsolationConfig(scan: ScanResult, outDir: string): string[];
|
|
10
|
+
export declare function writeRoleLayeringConfig(scan: ScanResult, outDir: string, statuses?: readonly GenerationStatus[]): string[];
|
|
11
|
+
export declare function writeDeepRelativeConfig(scan: ScanResult, outDir: string): string[];
|
|
12
|
+
export declare function writeConsoleIsolationConfig(scan: ScanResult, outDir: string): string[];
|
|
13
|
+
export declare function writeServerClientConfig(scan: ScanResult, outDir: string): string[];
|
|
14
|
+
export declare function writeUiDataConfig(scan: ScanResult, outDir: string): string[];
|
|
15
|
+
export declare function writeStoriesIsolationConfig(scan: ScanResult, outDir: string): string[];
|
|
16
|
+
export declare function writeWorkspacePackageConfig(scan: ScanResult, outDir: string): string[];
|
|
17
|
+
export declare function writeEnvAccessConfig(scan: ScanResult, outDir: string): string[];
|
|
18
|
+
export declare function writePhantomDependencyConfig(scan: ScanResult, outDir: string): string[];
|
|
19
|
+
export declare function writeEntryPurityConfig(scan: ScanResult, outDir: string): string[];
|
|
20
|
+
export declare function writeDependencyInternalsConfig(scan: ScanResult, outDir: string): string[];
|
|
21
|
+
export declare function writeEslintPlugin(scan: ScanResult, outDir: string): string[];
|
|
22
|
+
export declare function writeGraph(scan: ScanResult, outDir: string): string[];
|
|
23
|
+
export interface WrittenConfig {
|
|
24
|
+
files: string[];
|
|
25
|
+
label: string | null;
|
|
26
|
+
}
|
|
27
|
+
export declare function writeEnforcementConfigs(scan: ScanResult, outDir: string, options?: {
|
|
28
|
+
structural?: boolean;
|
|
29
|
+
}): WrittenConfig[];
|
|
30
|
+
export declare function regenerateConfigs(scan: ScanResult, outDir: string, options: {
|
|
31
|
+
structural?: boolean;
|
|
32
|
+
version: string;
|
|
33
|
+
}): {
|
|
34
|
+
configs: WrittenConfig[];
|
|
35
|
+
removed: string[];
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { toDependencyCruiser, toEslintBoundaries } from '../generator/layer-emitters.js';
|
|
4
|
+
import { toDependencyCruiserPublicApi } from '../generator/public-api-emitters.js';
|
|
5
|
+
import { toDependencyCruiserFeatureSlice } from '../generator/feature-slice-emitters.js';
|
|
6
|
+
import { toDependencyCruiserTestIsolation } from '../generator/test-isolation-emitters.js';
|
|
7
|
+
import { toDependencyCruiserAppIsolation } from '../generator/app-isolation-emitters.js';
|
|
8
|
+
import { toDependencyCruiserDependencyInternals } from '../generator/dependency-internals-emitters.js';
|
|
9
|
+
import { toDependencyCruiserRoleLayering } from '../generator/role-layering-emitters.js';
|
|
10
|
+
import { toDependencyCruiserEntryPurity } from '../generator/entry-purity-emitters.js';
|
|
11
|
+
import { toDependencyCruiserPhantomDependencies } from '../generator/phantom-dependency-emitters.js';
|
|
12
|
+
import { toEslintDeepRelative } from '../generator/deep-relative-emitters.js';
|
|
13
|
+
import { toEslintConsoleIsolation } from '../generator/console-isolation-emitters.js';
|
|
14
|
+
import { toEslintEnvAccess } from '../generator/env-access-emitters.js';
|
|
15
|
+
import { toEslintWorkspacePackageApi } from '../generator/workspace-package-emitters.js';
|
|
16
|
+
import { toDependencyCruiserStoriesIsolation } from '../generator/stories-isolation-emitters.js';
|
|
17
|
+
import { toDependencyCruiserUiData } from '../generator/ui-data-isolation-emitters.js';
|
|
18
|
+
import { toDependencyCruiserServerClient } from '../generator/server-client-emitters.js';
|
|
19
|
+
import { toGraphviz, toMermaid } from '../generator/graph-emitters.js';
|
|
20
|
+
import { emitRuleArtifacts } from '../generator/rule-generator.js';
|
|
21
|
+
import { buildForbiddenImportSpecs, renderEslintPluginSource, } from '../generator/eslint-plugin-emitter.js';
|
|
22
|
+
import { cleanPreviousOutputs, removeIfEmpty, writeOutputsManifest } from './outputs-manifest.js';
|
|
23
|
+
import { hasDependencyCruiserBlocks, hasEslintOutputs, writeDependencyCruiserAggregate, writeEslintAggregator, } from './wiring.js';
|
|
24
|
+
export function writeRules(scan, outDir, statuses = ['AUTO']) {
|
|
25
|
+
const written = [];
|
|
26
|
+
for (const pattern of scan.patterns) {
|
|
27
|
+
if (!statuses.includes(pattern.result.gate.status))
|
|
28
|
+
continue;
|
|
29
|
+
written.push(emitOne(pattern, scan.appDir, outDir));
|
|
30
|
+
}
|
|
31
|
+
return written;
|
|
32
|
+
}
|
|
33
|
+
export function emitOne(pattern, appDir, outDir) {
|
|
34
|
+
return emitRuleArtifacts(pattern.config, pattern.result, outDir, `archprint scan ${appDir}`);
|
|
35
|
+
}
|
|
36
|
+
export function writeLayerConfig(scan, outDir, statuses = ['AUTO']) {
|
|
37
|
+
const dependencyCruiser = toDependencyCruiser(scan.layerBoundaries, statuses);
|
|
38
|
+
if (dependencyCruiser.forbidden.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
mkdirSync(outDir, { recursive: true });
|
|
41
|
+
const write = (name, config) => {
|
|
42
|
+
const file = path.join(outDir, name);
|
|
43
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
44
|
+
return file;
|
|
45
|
+
};
|
|
46
|
+
return [
|
|
47
|
+
write('dependency-cruiser.archprint.json', dependencyCruiser),
|
|
48
|
+
write('eslint-boundaries.archprint.json', toEslintBoundaries(scan.layerBoundaries, statuses)),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
export function writePublicApiConfig(scan, outDir, statuses = ['AUTO']) {
|
|
52
|
+
const config = toDependencyCruiserPublicApi(scan.publicApi.groups, statuses);
|
|
53
|
+
if (config.forbidden.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
mkdirSync(outDir, { recursive: true });
|
|
56
|
+
const file = path.join(outDir, 'dependency-cruiser.public-api.archprint.json');
|
|
57
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
58
|
+
return [file];
|
|
59
|
+
}
|
|
60
|
+
export function writeFeatureSliceConfig(scan, outDir, statuses = ['AUTO']) {
|
|
61
|
+
const config = toDependencyCruiserFeatureSlice(scan.featureSlices.groups, statuses);
|
|
62
|
+
if (config.forbidden.length === 0)
|
|
63
|
+
return [];
|
|
64
|
+
mkdirSync(outDir, { recursive: true });
|
|
65
|
+
const file = path.join(outDir, 'dependency-cruiser.feature-slice.archprint.json');
|
|
66
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
67
|
+
return [file];
|
|
68
|
+
}
|
|
69
|
+
export function writeAppIsolationConfig(scan, outDir, statuses = ['AUTO']) {
|
|
70
|
+
const config = toDependencyCruiserAppIsolation(scan.appIsolation.groups, statuses);
|
|
71
|
+
if (config.forbidden.length === 0)
|
|
72
|
+
return [];
|
|
73
|
+
mkdirSync(outDir, { recursive: true });
|
|
74
|
+
const file = path.join(outDir, 'dependency-cruiser.app-isolation.archprint.json');
|
|
75
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
76
|
+
return [file];
|
|
77
|
+
}
|
|
78
|
+
export function writeTestIsolationConfig(scan, outDir) {
|
|
79
|
+
const config = toDependencyCruiserTestIsolation(scan.testIsolation);
|
|
80
|
+
if (config.forbidden.length === 0)
|
|
81
|
+
return [];
|
|
82
|
+
mkdirSync(outDir, { recursive: true });
|
|
83
|
+
const file = path.join(outDir, 'dependency-cruiser.test-isolation.archprint.json');
|
|
84
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
85
|
+
return [file];
|
|
86
|
+
}
|
|
87
|
+
export function writeRoleLayeringConfig(scan, outDir, statuses = ['AUTO']) {
|
|
88
|
+
const config = toDependencyCruiserRoleLayering(scan.roleLayering.boundaries, statuses);
|
|
89
|
+
if (config.forbidden.length === 0)
|
|
90
|
+
return [];
|
|
91
|
+
mkdirSync(outDir, { recursive: true });
|
|
92
|
+
const file = path.join(outDir, 'dependency-cruiser.role-layering.archprint.json');
|
|
93
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
94
|
+
return [file];
|
|
95
|
+
}
|
|
96
|
+
export function writeDeepRelativeConfig(scan, outDir) {
|
|
97
|
+
const config = toEslintDeepRelative(scan.deepRelative);
|
|
98
|
+
if (config === null)
|
|
99
|
+
return [];
|
|
100
|
+
mkdirSync(outDir, { recursive: true });
|
|
101
|
+
const file = path.join(outDir, 'eslint.deep-relative.archprint.json');
|
|
102
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
103
|
+
return [file];
|
|
104
|
+
}
|
|
105
|
+
export function writeConsoleIsolationConfig(scan, outDir) {
|
|
106
|
+
const config = toEslintConsoleIsolation(scan.consoleIsolation);
|
|
107
|
+
if (config === null)
|
|
108
|
+
return [];
|
|
109
|
+
mkdirSync(outDir, { recursive: true });
|
|
110
|
+
const file = path.join(outDir, 'eslint.console-isolation.archprint.json');
|
|
111
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
112
|
+
return [file];
|
|
113
|
+
}
|
|
114
|
+
export function writeServerClientConfig(scan, outDir) {
|
|
115
|
+
const config = toDependencyCruiserServerClient(scan.serverClient);
|
|
116
|
+
if (config.forbidden.length === 0)
|
|
117
|
+
return [];
|
|
118
|
+
mkdirSync(outDir, { recursive: true });
|
|
119
|
+
const file = path.join(outDir, 'dependency-cruiser.server-client.archprint.json');
|
|
120
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
121
|
+
return [file];
|
|
122
|
+
}
|
|
123
|
+
export function writeUiDataConfig(scan, outDir) {
|
|
124
|
+
const config = toDependencyCruiserUiData(scan.uiDataIsolation);
|
|
125
|
+
if (config.forbidden.length === 0)
|
|
126
|
+
return [];
|
|
127
|
+
mkdirSync(outDir, { recursive: true });
|
|
128
|
+
const file = path.join(outDir, 'dependency-cruiser.ui-data.archprint.json');
|
|
129
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
130
|
+
return [file];
|
|
131
|
+
}
|
|
132
|
+
export function writeStoriesIsolationConfig(scan, outDir) {
|
|
133
|
+
const config = toDependencyCruiserStoriesIsolation(scan.storiesIsolation);
|
|
134
|
+
if (config.forbidden.length === 0)
|
|
135
|
+
return [];
|
|
136
|
+
mkdirSync(outDir, { recursive: true });
|
|
137
|
+
const file = path.join(outDir, 'dependency-cruiser.stories-isolation.archprint.json');
|
|
138
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
139
|
+
return [file];
|
|
140
|
+
}
|
|
141
|
+
export function writeWorkspacePackageConfig(scan, outDir) {
|
|
142
|
+
const config = toEslintWorkspacePackageApi(scan.workspacePackageApi);
|
|
143
|
+
if (config === null)
|
|
144
|
+
return [];
|
|
145
|
+
mkdirSync(outDir, { recursive: true });
|
|
146
|
+
const file = path.join(outDir, 'eslint.workspace-package.archprint.json');
|
|
147
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
148
|
+
return [file];
|
|
149
|
+
}
|
|
150
|
+
export function writeEnvAccessConfig(scan, outDir) {
|
|
151
|
+
const config = toEslintEnvAccess(scan.envAccess);
|
|
152
|
+
if (config === null)
|
|
153
|
+
return [];
|
|
154
|
+
mkdirSync(outDir, { recursive: true });
|
|
155
|
+
const file = path.join(outDir, 'eslint.env-access.archprint.json');
|
|
156
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
157
|
+
return [file];
|
|
158
|
+
}
|
|
159
|
+
export function writePhantomDependencyConfig(scan, outDir) {
|
|
160
|
+
const config = toDependencyCruiserPhantomDependencies(scan.phantomDependencies);
|
|
161
|
+
if (config.forbidden.length === 0)
|
|
162
|
+
return [];
|
|
163
|
+
mkdirSync(outDir, { recursive: true });
|
|
164
|
+
const file = path.join(outDir, 'dependency-cruiser.phantom-deps.archprint.json');
|
|
165
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
166
|
+
return [file];
|
|
167
|
+
}
|
|
168
|
+
export function writeEntryPurityConfig(scan, outDir) {
|
|
169
|
+
const config = toDependencyCruiserEntryPurity(scan.entryPurity);
|
|
170
|
+
if (config.forbidden.length === 0)
|
|
171
|
+
return [];
|
|
172
|
+
mkdirSync(outDir, { recursive: true });
|
|
173
|
+
const file = path.join(outDir, 'dependency-cruiser.entry-purity.archprint.json');
|
|
174
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
175
|
+
return [file];
|
|
176
|
+
}
|
|
177
|
+
export function writeDependencyInternalsConfig(scan, outDir) {
|
|
178
|
+
const config = toDependencyCruiserDependencyInternals(scan.dependencyInternals);
|
|
179
|
+
if (config.forbidden.length === 0)
|
|
180
|
+
return [];
|
|
181
|
+
mkdirSync(outDir, { recursive: true });
|
|
182
|
+
const file = path.join(outDir, 'dependency-cruiser.dependency-internals.archprint.json');
|
|
183
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
184
|
+
return [file];
|
|
185
|
+
}
|
|
186
|
+
export function writeEslintPlugin(scan, outDir) {
|
|
187
|
+
const specs = buildForbiddenImportSpecs(scan.patterns);
|
|
188
|
+
if (specs.length === 0)
|
|
189
|
+
return [];
|
|
190
|
+
mkdirSync(outDir, { recursive: true });
|
|
191
|
+
const file = path.join(outDir, 'eslint-plugin.archprint.mjs');
|
|
192
|
+
writeFileSync(file, renderEslintPluginSource(specs));
|
|
193
|
+
return [file];
|
|
194
|
+
}
|
|
195
|
+
export function writeGraph(scan, outDir) {
|
|
196
|
+
if (scan.layerBoundaries.length === 0)
|
|
197
|
+
return [];
|
|
198
|
+
mkdirSync(outDir, { recursive: true });
|
|
199
|
+
const write = (name, content) => {
|
|
200
|
+
const file = path.join(outDir, name);
|
|
201
|
+
writeFileSync(file, `${content}\n`);
|
|
202
|
+
return file;
|
|
203
|
+
};
|
|
204
|
+
return [
|
|
205
|
+
write('layer-graph.archprint.mmd', toMermaid(scan.layerBoundaries)),
|
|
206
|
+
write('layer-graph.archprint.dot', toGraphviz(scan.layerBoundaries)),
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
const countAuto = (items) => items.filter((item) => item.gate.status === 'AUTO').length;
|
|
210
|
+
export function writeEnforcementConfigs(scan, outDir, options = {}) {
|
|
211
|
+
const structural = options.structural ?? false;
|
|
212
|
+
const configs = [];
|
|
213
|
+
const add = (files, label) => {
|
|
214
|
+
if (files.length > 0)
|
|
215
|
+
configs.push({ files, label });
|
|
216
|
+
};
|
|
217
|
+
add(writeRules(scan, outDir, ['AUTO']), null);
|
|
218
|
+
add(writeEslintPlugin(scan, outDir), 'forbidden-import rules as a loadable eslint plugin');
|
|
219
|
+
if (structural) {
|
|
220
|
+
add(writeLayerConfig(scan, outDir, ['AUTO']), `${countAuto(scan.layerBoundaries)} layer boundaries: dependency-cruiser and eslint-plugin-boundaries`);
|
|
221
|
+
add(writeRoleLayeringConfig(scan, outDir, ['AUTO']), `${countAuto(scan.roleLayering.boundaries)} role-layering boundaries: dependency-cruiser rules`);
|
|
222
|
+
}
|
|
223
|
+
add(writePublicApiConfig(scan, outDir, ['AUTO']), `${countAuto(scan.publicApi.groups)} public API boundaries: dependency-cruiser deep-import rules`);
|
|
224
|
+
if (structural) {
|
|
225
|
+
add(writeFeatureSliceConfig(scan, outDir, ['AUTO']), `${countAuto(scan.featureSlices.groups)} feature-slice boundaries: dependency-cruiser cross-slice rules`);
|
|
226
|
+
add(writeAppIsolationConfig(scan, outDir, ['AUTO']), `${countAuto(scan.appIsolation.groups)} app boundaries: dependency-cruiser cross-app rules`);
|
|
227
|
+
}
|
|
228
|
+
add(writeTestIsolationConfig(scan, outDir), 'test isolation: dependency-cruiser not-to-test rule');
|
|
229
|
+
add(writeDependencyInternalsConfig(scan, outDir), 'dependency hygiene: dependency-cruiser no-internals rule');
|
|
230
|
+
if (structural)
|
|
231
|
+
add(writeEntryPurityConfig(scan, outDir), 'entry purity: dependency-cruiser no-import-entry rule');
|
|
232
|
+
add(writePhantomDependencyConfig(scan, outDir), 'dependency declaration: dependency-cruiser no-phantom-deps rule');
|
|
233
|
+
add(writeDeepRelativeConfig(scan, outDir), 'import style: eslint no-restricted-imports rule');
|
|
234
|
+
add(writeConsoleIsolationConfig(scan, outDir), 'console isolation: eslint no-console rule');
|
|
235
|
+
if (structural) {
|
|
236
|
+
add(writeEnvAccessConfig(scan, outDir), 'env access: eslint no-restricted-properties rule');
|
|
237
|
+
add(writeWorkspacePackageConfig(scan, outDir), 'workspace package API: eslint no-restricted-imports rule');
|
|
238
|
+
add(writeStoriesIsolationConfig(scan, outDir), 'stories isolation: dependency-cruiser no-import-stories rule');
|
|
239
|
+
add(writeUiDataConfig(scan, outDir), 'UI / data separation: dependency-cruiser no-ui-to-data rule');
|
|
240
|
+
add(writeServerClientConfig(scan, outDir), 'server / client boundary: dependency-cruiser no-server-only-in-client rule');
|
|
241
|
+
}
|
|
242
|
+
add(writeGraph(scan, outDir), 'layer dependency graph: Mermaid and Graphviz DOT');
|
|
243
|
+
return configs;
|
|
244
|
+
}
|
|
245
|
+
export function regenerateConfigs(scan, outDir, options) {
|
|
246
|
+
const removed = cleanPreviousOutputs(outDir);
|
|
247
|
+
const configs = writeEnforcementConfigs(scan, outDir, { structural: options.structural });
|
|
248
|
+
const allPaths = configs.flatMap((config) => config.files);
|
|
249
|
+
if (hasEslintOutputs(allPaths))
|
|
250
|
+
allPaths.push(writeEslintAggregator(outDir));
|
|
251
|
+
if (hasDependencyCruiserBlocks(allPaths))
|
|
252
|
+
allPaths.push(writeDependencyCruiserAggregate(outDir));
|
|
253
|
+
if (allPaths.length > 0)
|
|
254
|
+
writeOutputsManifest(outDir, allPaths, options.version);
|
|
255
|
+
else
|
|
256
|
+
removeIfEmpty(outDir);
|
|
257
|
+
return { configs, removed };
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=generate.js.map
|