deep-slop 1.4.1

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.
@@ -0,0 +1,450 @@
1
+ import { t as collectFiles } from "./discover-B_S_Fy2S.js";
2
+ import { i as toLines, n as extractImports, r as readFileContent } from "./file-utils-B_HFXhCs.js";
3
+ import { basename, dirname, extname, relative, resolve } from "node:path";
4
+
5
+ //#region src/engines/arch-constraints/index.ts
6
+ /** Build a diagnostic with common fields filled */
7
+ function diag(opts) {
8
+ return {
9
+ filePath: opts.filePath,
10
+ engine: "arch-constraints",
11
+ rule: opts.rule,
12
+ severity: opts.severity,
13
+ message: opts.message,
14
+ help: opts.help,
15
+ line: opts.line,
16
+ column: opts.column,
17
+ category: "architecture",
18
+ fixable: opts.fixable,
19
+ suggestion: opts.suggestion,
20
+ detail: opts.detail
21
+ };
22
+ }
23
+ /** Determine threshold multipliers based on file extension and naming convention */
24
+ function getThresholdMultiplier(filePath) {
25
+ const ext = extname(filePath);
26
+ const name = basename(filePath, ext);
27
+ if (filePath.endsWith(".d.ts")) return {
28
+ fileLocMultiplier: Infinity,
29
+ functionLocMultiplier: Infinity
30
+ };
31
+ if (ext === ".rs") return {
32
+ fileLocMultiplier: 2.5,
33
+ functionLocMultiplier: 1.5
34
+ };
35
+ if (ext === ".go") return {
36
+ fileLocMultiplier: 1.5,
37
+ functionLocMultiplier: 1
38
+ };
39
+ if (ext === ".tsx" || ext === ".jsx") {
40
+ if (/^[A-Z][a-zA-Z0-9]+$/.test(name)) return {
41
+ fileLocMultiplier: 1.5,
42
+ functionLocMultiplier: 2
43
+ };
44
+ return {
45
+ fileLocMultiplier: 1.5,
46
+ functionLocMultiplier: 1
47
+ };
48
+ }
49
+ return {
50
+ fileLocMultiplier: 1,
51
+ functionLocMultiplier: 1
52
+ };
53
+ }
54
+ /** Count imports per file, flag files exceeding maxCoupling threshold */
55
+ function detectHighCoupling(imports, filePath, maxCoupling) {
56
+ const results = [];
57
+ const couplingImports = imports.filter((imp) => !imp.isTypeOnly);
58
+ const uniqueSources = new Set(couplingImports.map((imp) => imp.source));
59
+ const couplingCount = uniqueSources.size;
60
+ if (couplingCount > maxCoupling) {
61
+ const firstImport = couplingImports[0];
62
+ const line = firstImport?.line ?? 1;
63
+ const col = firstImport?.raw ? firstImport.raw.indexOf(firstImport.source) + 1 : 1;
64
+ results.push(diag({
65
+ filePath,
66
+ rule: "arch-constraints/high-coupling",
67
+ severity: "warning",
68
+ message: `File has ${couplingCount} imports (max: ${maxCoupling}) — high coupling`,
69
+ help: "Split this file into focused modules with fewer dependencies. Each module should have a single responsibility. Consider extracting related logic into a separate module that this file and others can share.",
70
+ line,
71
+ column: col,
72
+ fixable: false,
73
+ suggestion: {
74
+ type: "refactor",
75
+ text: `/* Split into focused modules — ${couplingCount} imports exceeds threshold of ${maxCoupling} */`,
76
+ confidence: .7,
77
+ reason: `Files with many imports are tightly coupled to many other modules, making them hard to test, refactor, and reason about independently. Breaking them into smaller, focused modules reduces coupling and improves maintainability.`
78
+ },
79
+ detail: {
80
+ couplingCount,
81
+ threshold: maxCoupling,
82
+ sources: [...uniqueSources]
83
+ }
84
+ }));
85
+ }
86
+ return results;
87
+ }
88
+ /** Patterns indicating UI layer */
89
+ const UI_PATH_PATTERNS = [
90
+ /\/components?\//i,
91
+ /\/views?\//i,
92
+ /\/pages?\//i,
93
+ /\/ui\//i,
94
+ /\/screens?\//i,
95
+ /\.(tsx|jsx)$/i
96
+ ];
97
+ /** Patterns indicating data/DB layer */
98
+ const DB_IMPORT_PATTERNS = [
99
+ /(?:^|\.\.\/|\.\/)(?:db|database|models?|repositories?|entities?|knex|prisma|drizzle|sequelize|typeorm|mongoose)\//i,
100
+ /\/db\//i,
101
+ /\/lib\/db\//i,
102
+ /\/data\/(?:models?|repositories?|entities?)\//i
103
+ ];
104
+ /** Patterns indicating API routes */
105
+ const API_ROUTE_PATTERNS = [
106
+ /\/api\//i,
107
+ /\/routes?\//i,
108
+ /\/controllers?\//i,
109
+ /\/handlers?\//i,
110
+ /\/endpoints?\//i
111
+ ];
112
+ /** Patterns indicating service layer */
113
+ const SERVICE_IMPORT_PATTERNS = [
114
+ /\/services?\//i,
115
+ /\/use-?cases?\//i,
116
+ /\/business\//i,
117
+ /\/domain\//i
118
+ ];
119
+ function isUIFile(relPath) {
120
+ return UI_PATH_PATTERNS.some((p) => p.test(relPath));
121
+ }
122
+ function isAPIRoute(relPath) {
123
+ return API_ROUTE_PATTERNS.some((p) => p.test(relPath));
124
+ }
125
+ function isDBImport(source) {
126
+ return DB_IMPORT_PATTERNS.some((p) => p.test(source));
127
+ }
128
+ function isServiceImport(source) {
129
+ return SERVICE_IMPORT_PATTERNS.some((p) => p.test(source));
130
+ }
131
+ function detectLayerViolations(imports, filePath) {
132
+ const results = [];
133
+ if (isUIFile(filePath)) {
134
+ for (const imp of imports) if (isDBImport(imp.source)) {
135
+ const col = imp.raw.indexOf(imp.source) + 1;
136
+ results.push(diag({
137
+ filePath,
138
+ rule: "arch-constraints/layer-violation",
139
+ severity: "warning",
140
+ message: `UI component imports directly from data layer: '${imp.source}'`,
141
+ help: "UI components should not import from the data layer directly. Introduce a service/hook layer between UI and data. For React, use custom hooks; for Vue, use composables; for Angular, use services.",
142
+ line: imp.line,
143
+ column: col,
144
+ fixable: false,
145
+ suggestion: {
146
+ type: "refactor",
147
+ text: `/* Replace direct DB import '${imp.source}' with a service or hook layer */`,
148
+ confidence: .8,
149
+ reason: "Direct data-layer imports in UI components violate separation of concerns and make the UI fragile to data-layer changes. A service layer encapsulates data access and provides a stable API for the UI."
150
+ },
151
+ detail: {
152
+ violationType: "ui-to-data",
153
+ importSource: imp.source
154
+ }
155
+ }));
156
+ }
157
+ }
158
+ if (isAPIRoute(filePath)) {
159
+ for (const imp of imports) if (isDBImport(imp.source) && !isServiceImport(imp.source)) {
160
+ const col = imp.raw.indexOf(imp.source) + 1;
161
+ results.push(diag({
162
+ filePath,
163
+ rule: "arch-constraints/layer-violation",
164
+ severity: "warning",
165
+ message: `API route imports DB layer directly (bypasses service): '${imp.source}'`,
166
+ help: "API routes should delegate business logic to a service layer rather than importing DB models directly. This ensures consistent business rules, easier testing, and a clean separation between HTTP handling and data access.",
167
+ line: imp.line,
168
+ column: col,
169
+ fixable: false,
170
+ suggestion: {
171
+ type: "refactor",
172
+ text: `/* Route '${imp.source}' through a service layer instead of direct DB import */`,
173
+ confidence: .75,
174
+ reason: "Bypassing the service layer in API routes couples HTTP handling to data access, making it hard to change the data layer or reuse business logic. A service layer provides a single source of truth for business rules."
175
+ },
176
+ detail: {
177
+ violationType: "api-bypasses-service",
178
+ importSource: imp.source
179
+ }
180
+ }));
181
+ }
182
+ }
183
+ return results;
184
+ }
185
+ /** Count named exports in a file (export function, export const, export class, etc.) */
186
+ function countExports(lines) {
187
+ let count = 0;
188
+ for (const { text } of lines) {
189
+ const trimmed = text.trim();
190
+ if (/^export\s+(?:default\s+)?(?:function|const|let|var|class|interface|type|enum)\s+/.test(trimmed)) count++;
191
+ const namedExport = trimmed.match(/^export\s+\{([^}]+)\}/);
192
+ if (namedExport) count += namedExport[1].split(",").filter((s) => s.trim()).length;
193
+ if (/^export\s+default\s+/.test(trimmed) && !/^export\s+default\s+function/.test(trimmed) && !/^export\s+default\s+class/.test(trimmed)) count++;
194
+ }
195
+ return count;
196
+ }
197
+ function detectGodFile(lineCount, exportCount, filePath, maxFileLoc) {
198
+ const results = [];
199
+ const GOD_EXPORT_THRESHOLD = 5;
200
+ if (lineCount > maxFileLoc && exportCount > GOD_EXPORT_THRESHOLD) results.push(diag({
201
+ filePath,
202
+ rule: "arch-constraints/god-file",
203
+ severity: "warning",
204
+ message: `God file: ${lineCount} lines (max: ${maxFileLoc}) with ${exportCount} exports (threshold: ${GOD_EXPORT_THRESHOLD})`,
205
+ help: "Split this file into smaller, focused modules. Files that are both large and export many things violate the Single Responsibility Principle. Each module should do one thing well.",
206
+ line: 1,
207
+ column: 1,
208
+ fixable: false,
209
+ suggestion: {
210
+ type: "refactor",
211
+ text: `/* Split this ${lineCount}-line file with ${exportCount} exports into focused modules */`,
212
+ confidence: .8,
213
+ reason: `Files with ${lineCount} lines and ${exportCount} exports are doing too much. Splitting improves readability, testability, and makes it easier for multiple developers to work on the codebase simultaneously.`
214
+ },
215
+ detail: {
216
+ lineCount,
217
+ exportCount,
218
+ maxFileLoc,
219
+ exportThreshold: GOD_EXPORT_THRESHOLD
220
+ }
221
+ }));
222
+ return results;
223
+ }
224
+ /** Build import graph from all files (only relative imports) */
225
+ function buildImportGraph(fileImports, rootDir) {
226
+ const adjacency = /* @__PURE__ */ new Map();
227
+ for (const [filePath, imports] of fileImports) {
228
+ const deps = /* @__PURE__ */ new Set();
229
+ for (const imp of imports) if (imp.source.startsWith(".")) {
230
+ const resolved = resolve(dirname(filePath), imp.source);
231
+ deps.add(resolved);
232
+ }
233
+ adjacency.set(filePath, deps);
234
+ }
235
+ return { adjacency };
236
+ }
237
+ /** DFS-based cycle detection with path tracking */
238
+ function detectCycles(graph, maxDepth) {
239
+ const cycles = [];
240
+ const visited = /* @__PURE__ */ new Set();
241
+ const inStack = /* @__PURE__ */ new Set();
242
+ const stack = [];
243
+ function dfs(node) {
244
+ if (inStack.has(node)) {
245
+ const cycleStart = stack.indexOf(node);
246
+ if (cycleStart !== -1) {
247
+ const cyclePath = stack.slice(cycleStart);
248
+ cyclePath.push(node);
249
+ cycles.push({
250
+ cycle: cyclePath,
251
+ depth: cyclePath.length - 1
252
+ });
253
+ }
254
+ return;
255
+ }
256
+ if (visited.has(node)) return;
257
+ visited.add(node);
258
+ inStack.add(node);
259
+ stack.push(node);
260
+ if (stack.length <= maxDepth) {
261
+ const neighbors = graph.adjacency.get(node) ?? /* @__PURE__ */ new Set();
262
+ for (const neighbor of neighbors) dfs(neighbor);
263
+ }
264
+ stack.pop();
265
+ inStack.delete(node);
266
+ }
267
+ for (const node of graph.adjacency.keys()) dfs(node);
268
+ const seen = /* @__PURE__ */ new Set();
269
+ const unique = [];
270
+ for (const c of cycles) {
271
+ const key = [...c.cycle.slice(0, -1)].sort().join("→");
272
+ if (!seen.has(key)) {
273
+ seen.add(key);
274
+ unique.push(c);
275
+ }
276
+ }
277
+ return unique;
278
+ }
279
+ function reportCircularDependencies(cycles, rootDir) {
280
+ const results = [];
281
+ for (const { cycle, depth } of cycles) {
282
+ const involvedFiles = cycle.slice(0, -1);
283
+ const fullChain = `${involvedFiles.map((p) => relative(rootDir, p) || p).join(" → ")} → ${relative(rootDir, cycle[0]) || cycle[0]}`;
284
+ const firstFile = involvedFiles[0] ?? "";
285
+ const relFirst = relative(rootDir, firstFile) || firstFile;
286
+ results.push(diag({
287
+ filePath: relFirst,
288
+ rule: "arch-constraints/circular-dependency",
289
+ severity: "error",
290
+ message: `Circular dependency detected: ${fullChain}`,
291
+ help: "Break the cycle by extracting shared logic into a separate module that both files can import without creating a loop. Circular dependencies cause initialization order issues and make the module graph fragile.",
292
+ line: 1,
293
+ column: 1,
294
+ fixable: false,
295
+ suggestion: {
296
+ type: "refactor",
297
+ text: `/* Circular: ${fullChain} — extract shared code to break the cycle */`,
298
+ confidence: .95,
299
+ reason: "Circular dependencies create fragile coupling, can cause initialization order bugs, and make the module graph harder to reason about. Extracting the shared dependency into a third module breaks the cycle cleanly."
300
+ },
301
+ detail: {
302
+ cycle: involvedFiles.map((p) => relative(rootDir, p)),
303
+ depth
304
+ }
305
+ }));
306
+ }
307
+ return results;
308
+ }
309
+ /** Count maximum nesting depth of blocks (if/for/while/try/switch/catch/functions) */
310
+ function detectDeepNesting(lines, filePath, maxNesting) {
311
+ const results = [];
312
+ let currentDepth = 0;
313
+ let maxDepthReached = 0;
314
+ let maxDepthLine = 0;
315
+ for (const { num, text } of lines) {
316
+ const trimmed = text.trim();
317
+ if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) continue;
318
+ for (let i = 0; i < trimmed.length; i++) {
319
+ if (trimmed[i] === "{") {
320
+ currentDepth++;
321
+ if (currentDepth > maxDepthReached) {
322
+ maxDepthReached = currentDepth;
323
+ maxDepthLine = num;
324
+ }
325
+ }
326
+ if (trimmed[i] === "}") currentDepth = Math.max(0, currentDepth - 1);
327
+ }
328
+ }
329
+ if (maxDepthReached > maxNesting) results.push(diag({
330
+ filePath,
331
+ rule: "arch-constraints/deep-nesting",
332
+ severity: "warning",
333
+ message: `Maximum nesting depth ${maxDepthReached} exceeds threshold of ${maxNesting}`,
334
+ help: "Reduce nesting by using early returns, guard clauses, extracting nested logic into separate functions, or inverting conditions. Deep nesting makes code hard to follow and error-prone to modify.",
335
+ line: maxDepthLine,
336
+ column: 1,
337
+ fixable: false,
338
+ suggestion: {
339
+ type: "refactor",
340
+ text: `/* Reduce nesting depth from ${maxDepthReached} to ≤${maxNesting} using early returns or extracted functions */`,
341
+ confidence: .7,
342
+ reason: `Deep nesting (${maxDepthReached} levels) makes code difficult to read and maintain. Early returns (guard clauses) and function extraction flatten the structure and improve readability.`
343
+ },
344
+ detail: {
345
+ maxDepthReached,
346
+ threshold: maxNesting,
347
+ deepestLine: maxDepthLine
348
+ }
349
+ }));
350
+ return results;
351
+ }
352
+ /** Path segments that indicate unstable / work-in-progress code */
353
+ const UNSTABLE_PATTERNS = [
354
+ /\binternal\b/i,
355
+ /\bprivate\b/i,
356
+ /\btemp\b/i,
357
+ /\bhack\b/i,
358
+ /\bwip\b/i,
359
+ /\bexperimental\b/i,
360
+ /\bunstable\b/i,
361
+ /\bprototype\b/i
362
+ ];
363
+ function detectUnstableDependencies(imports, filePath) {
364
+ const results = [];
365
+ for (const imp of imports) {
366
+ if (!imp.source.startsWith(".")) continue;
367
+ for (const pattern of UNSTABLE_PATTERNS) if (pattern.test(imp.source)) {
368
+ const matchLabel = imp.source.match(pattern)?.[0] ?? "unstable";
369
+ const col = imp.raw.indexOf(imp.source) + 1;
370
+ results.push(diag({
371
+ filePath,
372
+ rule: "arch-constraints/unstable-dependency",
373
+ severity: "info",
374
+ message: `Import from unstable path: '${imp.source}' (contains '${matchLabel}')`,
375
+ help: `The import path contains '${matchLabel}', which typically indicates work-in-progress, internal, or temporary code. This dependency may change or disappear without notice. Consider depending on a stable, published API instead.`,
376
+ line: imp.line,
377
+ column: col,
378
+ fixable: false,
379
+ suggestion: {
380
+ type: "refactor",
381
+ text: `/* Consider replacing unstable import '${imp.source}' with a stable API */`,
382
+ confidence: .6,
383
+ reason: `Paths containing '${matchLabel}' signal provisional code that may be refactored or removed. Depending on it creates fragile coupling to code that isn't guaranteed to remain stable.`
384
+ },
385
+ detail: {
386
+ importSource: imp.source,
387
+ unstablePattern: matchLabel
388
+ }
389
+ }));
390
+ break;
391
+ }
392
+ }
393
+ return results;
394
+ }
395
+ const archConstraintsEngine = {
396
+ name: "arch-constraints",
397
+ description: "Architecture constraint validation: high coupling, layer violations, god files, circular dependencies, deep nesting, and unstable dependencies.",
398
+ supportedLanguages: ["typescript", "javascript"],
399
+ async run(context) {
400
+ const start = performance.now();
401
+ const { rootDirectory, config } = context;
402
+ const maxCoupling = config.quality.maxCoupling;
403
+ const maxFileLoc = config.quality.maxFileLoc;
404
+ const maxNesting = config.quality.maxNesting;
405
+ const files = await collectFiles(rootDirectory, ["typescript", "javascript"], config.exclude, context.files);
406
+ if (files.length === 0) return {
407
+ engine: this.name,
408
+ diagnostics: [],
409
+ elapsed: performance.now() - start,
410
+ skipped: true,
411
+ skipReason: "No TypeScript or JavaScript files found to scan."
412
+ };
413
+ const diagnostics = [];
414
+ const fileImportsMap = /* @__PURE__ */ new Map();
415
+ for (const absPath of files) {
416
+ const relPath = relative(rootDirectory, absPath);
417
+ let content;
418
+ try {
419
+ content = await readFileContent(absPath);
420
+ } catch {
421
+ continue;
422
+ }
423
+ const lines = toLines(content);
424
+ const imports = extractImports(content, "typescript");
425
+ fileImportsMap.set(absPath, imports);
426
+ const multiplier = getThresholdMultiplier(relPath);
427
+ const effectiveFileLoc = Math.round(maxFileLoc * multiplier.fileLocMultiplier);
428
+ diagnostics.push(...detectHighCoupling(imports, relPath, maxCoupling));
429
+ diagnostics.push(...detectLayerViolations(imports, relPath));
430
+ const lineCount = lines.length;
431
+ const exportCount = countExports(lines);
432
+ diagnostics.push(...detectGodFile(lineCount, exportCount, relPath, effectiveFileLoc));
433
+ diagnostics.push(...detectDeepNesting(lines, relPath, maxNesting));
434
+ diagnostics.push(...detectUnstableDependencies(imports, relPath));
435
+ }
436
+ const graph = buildImportGraph(fileImportsMap, rootDirectory);
437
+ const maxCircularDepth = config.imports.maxCircularDepth;
438
+ const cycles = detectCycles(graph, maxCircularDepth);
439
+ diagnostics.push(...reportCircularDependencies(cycles, rootDirectory));
440
+ return {
441
+ engine: this.name,
442
+ diagnostics,
443
+ elapsed: performance.now() - start,
444
+ skipped: false
445
+ };
446
+ }
447
+ };
448
+
449
+ //#endregion
450
+ export { archConstraintsEngine };