clckernel 1.2.6

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 (71) hide show
  1. package/AGENTS.md +124 -0
  2. package/README.es.md +166 -0
  3. package/README.md +167 -0
  4. package/bin/cli.js +12 -0
  5. package/docs/Journal/001-adr-clckernel-governance.md +18 -0
  6. package/package.json +44 -0
  7. package/src/adapters/astro.js +67 -0
  8. package/src/adapters/base.js +109 -0
  9. package/src/adapters/django.js +70 -0
  10. package/src/adapters/fastapi.js +69 -0
  11. package/src/adapters/go.js +50 -0
  12. package/src/adapters/laravel.js +52 -0
  13. package/src/adapters/nextjs.js +72 -0
  14. package/src/adapters/rails.js +54 -0
  15. package/src/adapters/rust.js +51 -0
  16. package/src/catalog.js +217 -0
  17. package/src/config.js +250 -0
  18. package/src/detector.js +85 -0
  19. package/src/doctor.js +125 -0
  20. package/src/generator.js +473 -0
  21. package/src/index.js +52 -0
  22. package/src/technologies/detector.js +243 -0
  23. package/src/technologies/guards/_shared.js +66 -0
  24. package/src/ui.js +101 -0
  25. package/src/yaml.js +173 -0
  26. package/test/adapters/astro.test.js +102 -0
  27. package/test/adapters/base.test.js +86 -0
  28. package/test/adapters/django.test.js +95 -0
  29. package/test/adapters/fastapi.test.js +88 -0
  30. package/test/adapters/go.test.js +81 -0
  31. package/test/adapters/laravel.test.js +106 -0
  32. package/test/adapters/nextjs.test.js +113 -0
  33. package/test/adapters/rails.test.js +108 -0
  34. package/test/adapters/rust.test.js +83 -0
  35. package/test/catalog.test.js +170 -0
  36. package/test/config.test.js +512 -0
  37. package/test/detector.test.js +345 -0
  38. package/test/doctor.test.js +302 -0
  39. package/test/generator.test.js +419 -0
  40. package/test/guards_new.test.js +95 -0
  41. package/test/helpers.js +36 -0
  42. package/test/integration/cli-flow.test.js +270 -0
  43. package/test/technologies/celery_guard.test.js +185 -0
  44. package/test/technologies/detector.test.js +342 -0
  45. package/test/technologies/docker_guard.test.js +187 -0
  46. package/test/technologies/integration.test.js +123 -0
  47. package/test/technologies/postgres_guard.test.js +87 -0
  48. package/test/technologies/redis_guard.test.js +128 -0
  49. package/tools/audit.js +219 -0
  50. package/tools/audit.py +273 -0
  51. package/tools/celery_guard.py +209 -0
  52. package/tools/check_a11y.js +109 -0
  53. package/tools/check_api_contracts.js +139 -0
  54. package/tools/check_architecture.js +139 -0
  55. package/tools/check_architecture.py +264 -0
  56. package/tools/check_custom.js +163 -0
  57. package/tools/check_custom.py +395 -0
  58. package/tools/check_db_efficiency.py +190 -0
  59. package/tools/check_migrations.py +223 -0
  60. package/tools/check_performance.js +142 -0
  61. package/tools/check_responsive.js +131 -0
  62. package/tools/check_scope.py +180 -0
  63. package/tools/check_seo.js +139 -0
  64. package/tools/check_storybook.js +108 -0
  65. package/tools/check_ui_reuse.js +135 -0
  66. package/tools/docker_guard.py +210 -0
  67. package/tools/postgres_guard.py +192 -0
  68. package/tools/redis_guard.py +197 -0
  69. package/tools/scan_secrets.js +109 -0
  70. package/tools/scan_secrets.py +153 -0
  71. package/tools/verify_tdd.py +212 -0
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Zod API Contract Guard
4
+ * Validates that API routes use Zod for request/response schema validation.
5
+ * Self-contained: uses only Node.js built-ins.
6
+ *
7
+ * @param {string} targetDir - Directory to scan (defaults to cwd)
8
+ * @returns {{ exitCode: number, skipped: boolean, name: string, data: object }}
9
+ */
10
+ const check = async function checkApiContracts(targetDir) {
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+
15
+ const dir = targetDir || process.cwd();
16
+
17
+ // Check if Zod is available
18
+ let hasZod = false;
19
+ const pkgPath = path.join(dir, 'package.json');
20
+ if (fs.existsSync(pkgPath)) {
21
+ try {
22
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
23
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
24
+ hasZod = Boolean(deps['zod']);
25
+ } catch { /* skip */ }
26
+ }
27
+
28
+ // Collect API route files
29
+ let files;
30
+ try {
31
+ const out = execSync('git diff --name-only HEAD~1', {
32
+ cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
33
+ });
34
+ files = out.split('\n').filter(f => /\.(ts|js|tsx|jsx)$/.test(f.trim()));
35
+ } catch {
36
+ try {
37
+ const out = execSync('git diff --name-only --cached', {
38
+ cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
39
+ });
40
+ files = out.split('\n').filter(f => /\.(ts|js|tsx|jsx)$/.test(f.trim()));
41
+ } catch {
42
+ return { exitCode: 0, skipped: true, name: 'check_api_contracts', data: { violations: {}, message: 'No git diff available' } };
43
+ }
44
+ }
45
+
46
+ if (!files || files.length === 0) {
47
+ return { exitCode: 0, skipped: false, name: 'check_api_contracts', data: { violations: {}, message: 'No files changed' } };
48
+ }
49
+
50
+ // Identify API route files (Next.js App Router or Pages Router)
51
+ const apiFiles = files.filter(f =>
52
+ f.includes('/api/') || f.includes('route.') || f.includes('/routes/')
53
+ );
54
+
55
+ if (apiFiles.length === 0) {
56
+ return { exitCode: 0, skipped: false, name: 'check_api_contracts', data: { violations: {}, message: 'No API route files changed' } };
57
+ }
58
+
59
+ const violations = {};
60
+
61
+ for (const file of apiFiles) {
62
+ const filePath = path.join(dir, file);
63
+ if (!fs.existsSync(filePath)) continue;
64
+
65
+ let content;
66
+ try {
67
+ content = fs.readFileSync(filePath, 'utf-8');
68
+ } catch {
69
+ continue;
70
+ }
71
+
72
+ const fileViolations = [];
73
+
74
+ // Check: API route handler without Zod validation
75
+ const hasHandler = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)/.test(content);
76
+ if (hasHandler) {
77
+ // Look for request body parsing without Zod
78
+ if (/await\s+req\.json\s*\(\s*\)/.test(content) && !/\.parse\s*\(|\.safeParse\s*\(/.test(content)) {
79
+ fileViolations.push({
80
+ lineNo: 0,
81
+ rule: 'no-body-validation',
82
+ message: 'API route parses request body without Zod validation — use z.object().parse() or .safeParse()',
83
+ });
84
+ }
85
+
86
+ // Look for NextRequest without Zod
87
+ if (/NextRequest/.test(content) && !/z\.|zod/.test(content)) {
88
+ fileViolations.push({
89
+ lineNo: 0,
90
+ rule: 'no-request-validation',
91
+ message: 'API route uses NextRequest without Zod schema validation',
92
+ });
93
+ }
94
+
95
+ // Check: response without schema validation
96
+ if (/Response\.json\s*\(/.test(content) && !/z\.|zod/.test(content) && hasZod) {
97
+ fileViolations.push({
98
+ lineNo: 0,
99
+ rule: 'no-response-validation',
100
+ message: 'API route returns JSON without Zod response schema — consider validating output',
101
+ });
102
+ }
103
+ }
104
+
105
+ // Check: fetch calls without response validation
106
+ if (/fetch\s*\(/.test(content) && !/z\.|zod/.test(content) && hasZod) {
107
+ fileViolations.push({
108
+ lineNo: 0,
109
+ rule: 'no-fetch-validation',
110
+ message: 'fetch() call without Zod response parsing — validate external API responses',
111
+ });
112
+ }
113
+
114
+ if (fileViolations.length > 0) {
115
+ violations[file] = fileViolations;
116
+ }
117
+ }
118
+
119
+ const totalCount = Object.values(violations).flat().length;
120
+ return {
121
+ exitCode: totalCount > 0 ? 1 : 0,
122
+ skipped: false,
123
+ name: 'check_api_contracts',
124
+ data: {
125
+ violations,
126
+ message: totalCount > 0
127
+ ? `${totalCount} API contract issue(s) found in ${Object.keys(violations).length} file(s)`
128
+ : 'All API contract checks passed',
129
+ },
130
+ };
131
+ };
132
+ module.exports = check;
133
+
134
+ if (require.main === module) {
135
+ check(process.argv[2]).then(result => {
136
+ console.log(JSON.stringify(result, null, 2));
137
+ process.exit(result.exitCode);
138
+ });
139
+ }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Next.js Architecture & Server Components Guard
4
+ * Validates Server Component boundaries, layout protection, and layer separation.
5
+ * Self-contained: uses only Node.js built-ins.
6
+ *
7
+ * @param {string} targetDir - Directory to scan (defaults to cwd)
8
+ * @returns {{ exitCode: number, skipped: boolean, name: string, data: object }}
9
+ */
10
+ const check = async function checkArchitecture(targetDir) {
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+
15
+ const dir = targetDir || process.cwd();
16
+
17
+ // Verify this is a Next.js project
18
+ const pkgPath = path.join(dir, 'package.json');
19
+ if (!fs.existsSync(pkgPath)) {
20
+ return { exitCode: 0, skipped: true, name: 'check_architecture', data: { violations: {}, message: 'No package.json found — skipping architecture check' } };
21
+ }
22
+
23
+ let isNext;
24
+ try {
25
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
26
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
27
+ isNext = Boolean(deps['next']);
28
+ } catch {
29
+ isNext = false;
30
+ }
31
+
32
+ if (!isNext) {
33
+ return { exitCode: 0, skipped: true, name: 'check_architecture', data: { violations: {}, message: 'Not a Next.js project — skipping' } };
34
+ }
35
+
36
+ // Collect changed files
37
+ let files;
38
+ try {
39
+ const out = execSync('git diff --name-only HEAD~1', {
40
+ cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
41
+ });
42
+ files = out.split('\n').filter(f => /\.(tsx|jsx|ts|js)$/.test(f.trim()));
43
+ } catch {
44
+ try {
45
+ const out = execSync('git diff --name-only --cached', {
46
+ cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
47
+ });
48
+ files = out.split('\n').filter(f => /\.(tsx|jsx|ts|js)$/.test(f.trim()));
49
+ } catch {
50
+ return { exitCode: 0, skipped: true, name: 'check_architecture', data: { violations: {}, message: 'No git diff available' } };
51
+ }
52
+ }
53
+
54
+ if (!files || files.length === 0) {
55
+ return { exitCode: 0, skipped: false, name: 'check_architecture', data: { violations: {}, message: 'No files changed' } };
56
+ }
57
+
58
+ const violations = {};
59
+
60
+ for (const file of files) {
61
+ const filePath = path.join(dir, file);
62
+ if (!fs.existsSync(filePath)) continue;
63
+
64
+ let content;
65
+ try {
66
+ content = fs.readFileSync(filePath, 'utf-8');
67
+ } catch {
68
+ continue;
69
+ }
70
+
71
+ const fileViolations = [];
72
+ const isLayout = /layout\.(tsx|jsx|ts|js)$/.test(file);
73
+ const isPage = /page\.(tsx|jsx|ts|js)$/.test(file);
74
+ const hasUseClient = /['"]use client['"]/.test(content);
75
+
76
+ // Check: layout.tsx should NOT have "use client"
77
+ if (isLayout && hasUseClient) {
78
+ fileViolations.push({ lineNo: 1, module: 'layout-boundary', reason: 'Layout must be a Server Component — remove "use client"' });
79
+ }
80
+
81
+ // Check: page.tsx should NOT have "use client" (prefer Server Components)
82
+ if (isPage && hasUseClient) {
83
+ fileViolations.push({ lineNo: 1, module: 'page-boundary', reason: 'Page uses "use client" — prefer Server Component unless client interactivity is required' });
84
+ }
85
+
86
+ // Check: files with "use client" importing server-only modules
87
+ if (hasUseClient) {
88
+ if (/require\s*\(\s*['"]fs['"]\s*\)/.test(content) || /from\s+['"]fs['"]/.test(content)) {
89
+ fileViolations.push({ lineNo: 0, module: 'server-in-client', reason: 'Client Component imports "fs" — use a Server Component or API route instead' });
90
+ }
91
+ if (/require\s*\(\s*['"]path['"]\s*\)/.test(content) || /from\s+['"]path['"]/.test(content)) {
92
+ fileViolations.push({ lineNo: 0, module: 'server-in-client', reason: 'Client Component imports "path" — move logic to Server Component' });
93
+ }
94
+ if (/require\s*\(\s*['"]child_process['"]\s*\)/.test(content) || /from\s+['"]child_process['"]/.test(content)) {
95
+ fileViolations.push({ lineNo: 0, module: 'server-in-client', reason: 'Client Component imports "child_process" — this is a server-only module' });
96
+ }
97
+ }
98
+
99
+ // Check: files without "use client" using useState/useEffect
100
+ if (!hasUseClient && (/\buseState\b/.test(content) || /\buseEffect\b/.test(content))) {
101
+ fileViolations.push({ lineNo: 0, module: 'missing-use-client', reason: 'Uses React hooks without "use client" directive — add directive or move to Client Component' });
102
+ }
103
+
104
+ // Check: files without "use client" using onClick/onSubmit handlers
105
+ if (!hasUseClient && /on(Click|Submit|Change|Focus|Blur)\s*=\s*\{/.test(content)) {
106
+ fileViolations.push({ lineNo: 0, module: 'event-handler', reason: 'Event handler in Server Component — move to Client Component with "use client"' });
107
+ }
108
+
109
+ // Check: "use server" should only be in API or action files
110
+ if (/['"]use server['"]/.test(content) && !/action|api|route/i.test(file)) {
111
+ fileViolations.push({ lineNo: 1, module: 'server-action-location', reason: '"use server" in non-action file — Server Actions belong in dedicated action files' });
112
+ }
113
+
114
+ if (fileViolations.length > 0) {
115
+ violations[file] = fileViolations;
116
+ }
117
+ }
118
+
119
+ const totalCount = Object.values(violations).flat().length;
120
+ return {
121
+ exitCode: totalCount > 0 ? 1 : 0,
122
+ skipped: false,
123
+ name: 'check_architecture',
124
+ data: {
125
+ violations,
126
+ message: totalCount > 0
127
+ ? `${totalCount} architecture issue(s) found in ${Object.keys(violations).length} file(s)`
128
+ : 'All architecture checks passed',
129
+ },
130
+ };
131
+ };
132
+ module.exports = check;
133
+
134
+ if (require.main === module) {
135
+ check(process.argv[2]).then(result => {
136
+ console.log(JSON.stringify(result, null, 2));
137
+ process.exit(result.exitCode);
138
+ });
139
+ }
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Clean Architecture / Django Layer Boundary Guard
4
+ Validates that Python code respects architectural layer boundaries:
5
+ - Domain layer should not import from Application or Infrastructure
6
+ - Application layer should not import from Infrastructure or Routes
7
+ - Routes/API should not import from Domain directly (go through Application)
8
+ For Django: checks that models don't import from views, views don't import from urls, etc.
9
+ Self-contained: uses only Python stdlib (ast, pathlib, subprocess, json).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import ast
14
+ import json
15
+ import os
16
+ import pathlib
17
+ import re
18
+ import subprocess
19
+ import sys
20
+
21
+
22
+ def _get_changed_py_files(target_dir: str) -> list[str] | None:
23
+ """Return changed .py files from git diff, or None on failure."""
24
+ for ref in ("HEAD~1", "--cached"):
25
+ try:
26
+ args = ["git", "diff", "--name-only"]
27
+ if ref == "--cached":
28
+ args.append("--cached")
29
+ else:
30
+ args.append(ref)
31
+ out = subprocess.check_output(
32
+ args, cwd=target_dir,
33
+ stderr=subprocess.DEVNULL, text=True,
34
+ )
35
+ files = [
36
+ f.strip() for f in out.splitlines()
37
+ if f.strip().endswith(".py")
38
+ ]
39
+ if files:
40
+ return files
41
+ except (subprocess.CalledProcessError, FileNotFoundError):
42
+ continue
43
+ return None
44
+
45
+
46
+ def _detect_framework(target_dir: str) -> str:
47
+ """Detect whether this is a Django, FastAPI, or generic Python project."""
48
+ root = pathlib.Path(target_dir)
49
+
50
+ if (root / "manage.py").exists():
51
+ return "django"
52
+
53
+ for name in ("pyproject.toml", "requirements.txt"):
54
+ fpath = root / name
55
+ if fpath.exists():
56
+ try:
57
+ text = fpath.read_text(encoding="utf-8", errors="replace").lower()
58
+ except OSError:
59
+ continue
60
+ if "django" in text:
61
+ return "django"
62
+ if "fastapi" in text:
63
+ return "fastapi"
64
+
65
+ return "generic"
66
+
67
+
68
+ def _check_django_layers(
69
+ filepath: str, content: str, tree: ast.Module,
70
+ ) -> list[dict]:
71
+ """Check Django layer boundary violations."""
72
+ violations: list[dict] = []
73
+ rel_path = filepath
74
+
75
+ # Determine file layer from path
76
+ if "/models" in rel_path or rel_path.endswith("/models.py"):
77
+ file_layer = "models"
78
+ elif "/views" in rel_path or rel_path.endswith("/views.py"):
79
+ file_layer = "views"
80
+ elif "/urls" in rel_path or rel_path.endswith("/urls.py"):
81
+ file_layer = "urls"
82
+ elif "/serializers" in rel_path or rel_path.endswith("/serializers.py"):
83
+ file_layer = "serializers"
84
+ elif "/forms" in rel_path or rel_path.endswith("/forms.py"):
85
+ file_layer = "forms"
86
+ elif "/admin" in rel_path or rel_path.endswith("/admin.py"):
87
+ file_layer = "admin"
88
+ elif "/tasks" in rel_path or rel_path.endswith("/tasks.py"):
89
+ file_layer = "tasks"
90
+ else:
91
+ return violations # Can't determine layer, skip
92
+
93
+ for node in ast.walk(tree):
94
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
95
+ if isinstance(node, ast.ImportFrom) and node.module:
96
+ imported = node.module
97
+ elif isinstance(node, ast.Import):
98
+ imported = node.names[0].name if node.names else ""
99
+ else:
100
+ continue
101
+
102
+ # Django models should not import from views, urls, admin
103
+ if file_layer == "models":
104
+ bad_targets = ("views", "urls", "admin", "serializers", "forms")
105
+ for bad in bad_targets:
106
+ if f".{bad}" in imported or imported == bad:
107
+ violations.append({
108
+ "lineNo": getattr(node, "lineno", 0),
109
+ "module": imported,
110
+ "reason": (
111
+ f"Model imports from {bad} layer — "
112
+ f"models must not depend on presentation or routing"
113
+ ),
114
+ })
115
+
116
+ # Views should not import from other views or urls
117
+ if file_layer == "views":
118
+ if ".urls" in imported or imported.endswith("urls"):
119
+ violations.append({
120
+ "lineNo": getattr(node, "lineno", 0),
121
+ "module": imported,
122
+ "reason": "View imports from urls — views should not depend on URL configuration",
123
+ })
124
+
125
+ # URLs should not import from models directly
126
+ if file_layer == "urls":
127
+ if ".models" in imported or imported.endswith("models"):
128
+ violations.append({
129
+ "lineNo": getattr(node, "lineno", 0),
130
+ "module": imported,
131
+ "reason": "URL module imports models directly — route through views",
132
+ })
133
+
134
+ return violations
135
+
136
+
137
+ def _check_clean_architecture(
138
+ filepath: str, content: str, tree: ast.Module,
139
+ ) -> list[dict]:
140
+ """Check Clean Architecture boundary violations for generic Python projects."""
141
+ violations: list[dict] = []
142
+
143
+ # Detect layer from path
144
+ if "/domain" in filepath:
145
+ file_layer = "domain"
146
+ elif "/application" in filepath or "/use_cases" in filepath:
147
+ file_layer = "application"
148
+ elif "/infrastructure" in filepath or "/adapters" in filepath:
149
+ file_layer = "infrastructure"
150
+ elif "/routes" in filepath or "/api" in filepath:
151
+ file_layer = "routes"
152
+ else:
153
+ return violations # Can't determine layer
154
+
155
+ for node in ast.walk(tree):
156
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
157
+ if isinstance(node, ast.ImportFrom) and node.module:
158
+ imported = node.module
159
+ elif isinstance(node, ast.Import):
160
+ imported = node.names[0].name if node.names else ""
161
+ else:
162
+ continue
163
+
164
+ # Domain must not import from application, infrastructure, or routes
165
+ if file_layer == "domain":
166
+ bad = ("application", "use_cases", "infrastructure", "adapters", "routes", "api")
167
+ for b in bad:
168
+ if f".{b}" in imported or imported.startswith(b):
169
+ violations.append({
170
+ "lineNo": getattr(node, "lineno", 0),
171
+ "module": imported,
172
+ "reason": (
173
+ f"Domain layer imports from '{b}' — "
174
+ f"domain must be framework-independent"
175
+ ),
176
+ })
177
+
178
+ # Application must not import from infrastructure or routes
179
+ if file_layer == "application":
180
+ bad = ("infrastructure", "adapters", "routes", "api")
181
+ for b in bad:
182
+ if f".{b}" in imported or imported.startswith(b):
183
+ violations.append({
184
+ "lineNo": getattr(node, "lineno", 0),
185
+ "module": imported,
186
+ "reason": (
187
+ f"Application layer imports from '{b}' — "
188
+ f"use dependency inversion"
189
+ ),
190
+ })
191
+
192
+ return violations
193
+
194
+
195
+ def check(target_dir: str | None = None) -> dict:
196
+ """
197
+ Validate architecture layer boundaries in changed Python files.
198
+
199
+ Returns:
200
+ dict with keys: exit_code, skipped, name, data
201
+ data.violations maps filepath -> list of {lineNo, module, reason}
202
+ """
203
+ dir_path = target_dir or os.getcwd()
204
+
205
+ files = _get_changed_py_files(dir_path)
206
+ if files is None:
207
+ return {
208
+ "exit_code": 0, "skipped": True, "name": "check_architecture",
209
+ "data": {"violations": {}, "message": "No git diff available"},
210
+ }
211
+
212
+ if not files:
213
+ return {
214
+ "exit_code": 0, "skipped": False, "name": "check_architecture",
215
+ "data": {"violations": {}, "message": "No Python files changed"},
216
+ }
217
+
218
+ framework = _detect_framework(dir_path)
219
+ violations: dict[str, list[dict]] = {}
220
+
221
+ for filepath in files:
222
+ full_path = pathlib.Path(dir_path) / filepath
223
+ if not full_path.exists():
224
+ continue
225
+
226
+ try:
227
+ content = full_path.read_text(encoding="utf-8", errors="replace")
228
+ except (OSError, PermissionError):
229
+ continue
230
+
231
+ try:
232
+ tree = ast.parse(content, filename=filepath)
233
+ except SyntaxError:
234
+ continue
235
+
236
+ if framework == "django":
237
+ file_violations = _check_django_layers(filepath, content, tree)
238
+ else:
239
+ file_violations = _check_clean_architecture(filepath, content, tree)
240
+
241
+ if file_violations:
242
+ violations[filepath] = file_violations
243
+
244
+ total = sum(len(v) for v in violations.values())
245
+ if total:
246
+ msg = (
247
+ f"{total} architecture violation(s) found in "
248
+ f"{len(violations)} file(s)"
249
+ )
250
+ else:
251
+ msg = "All architecture checks passed"
252
+
253
+ return {
254
+ "exit_code": 1 if total else 0,
255
+ "skipped": False,
256
+ "name": "check_architecture",
257
+ "data": {"violations": violations, "message": msg},
258
+ }
259
+
260
+
261
+ if __name__ == "__main__":
262
+ result = check(sys.argv[1] if len(sys.argv) > 1 else None)
263
+ print(json.dumps(result, indent=2))
264
+ sys.exit(result["exit_code"])