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,358 @@
1
+ import { t as collectFiles } from "./discover-B_S_Fy2S.js";
2
+ import { r as readFileContent } from "./file-utils-B_HFXhCs.js";
3
+ import { join, relative } from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ import { minimatch } from "minimatch";
6
+ import yaml from "js-yaml";
7
+
8
+ //#region src/engines/arch-rules/loader.ts
9
+ const RULES_PATH = ".deep-slop/rules.yml";
10
+ /** Validate and normalize a single rule object */
11
+ function validateRule(raw, index) {
12
+ const name = typeof raw.name === "string" ? raw.name : `rule-${index + 1}`;
13
+ const type = raw.type;
14
+ if (!type || ![
15
+ "forbid_import",
16
+ "forbid_import_from_path",
17
+ "require_pattern"
18
+ ].includes(type)) throw new Error(`Rule "${name}" has invalid type: "${type}". Expected: forbid_import, forbid_import_from_path, require_pattern`);
19
+ if (!raw.match || typeof raw.match !== "string") throw new Error(`Rule "${name}" is missing required "match" glob pattern`);
20
+ if (type === "forbid_import" && (!raw.forbid || typeof raw.forbid !== "string")) throw new Error(`Rule "${name}" (forbid_import) is missing required "forbid" field`);
21
+ if (type === "forbid_import_from_path" && (!raw.forbid || typeof raw.forbid !== "string")) throw new Error(`Rule "${name}" (forbid_import_from_path) is missing required "forbid" field`);
22
+ if (type === "forbid_import_from_path" && (!raw.from || typeof raw.from !== "string")) throw new Error(`Rule "${name}" (forbid_import_from_path) is missing required "from" field`);
23
+ if (type === "require_pattern" && (!raw.pattern || typeof raw.pattern !== "string")) throw new Error(`Rule "${name}" (require_pattern) is missing required "pattern" field`);
24
+ const severity = raw.severity;
25
+ if (severity && ![
26
+ "error",
27
+ "warning",
28
+ "info"
29
+ ].includes(severity)) throw new Error(`Rule "${name}" has invalid severity: "${severity}". Expected: error, warning, info`);
30
+ return {
31
+ name,
32
+ type,
33
+ match: raw.match,
34
+ forbid: raw.forbid,
35
+ from: raw.from,
36
+ pattern: raw.pattern,
37
+ severity: severity ?? "warning",
38
+ where: raw.where
39
+ };
40
+ }
41
+ /**
42
+ * Load architecture rules from .deep-slop/rules.yml.
43
+ * Returns an empty array if the file does not exist.
44
+ */
45
+ async function loadRules(rootDir) {
46
+ const filePath = join(rootDir, RULES_PATH);
47
+ let content;
48
+ try {
49
+ content = await readFile(filePath, "utf-8");
50
+ } catch {
51
+ return [];
52
+ }
53
+ const parsed = yaml.load(content);
54
+ if (!parsed || !Array.isArray(parsed.rules)) throw new Error(`Invalid rules file: ${RULES_PATH} — expected a "rules" array at top level`);
55
+ return parsed.rules.map((raw, i) => validateRule(raw, i));
56
+ }
57
+
58
+ //#endregion
59
+ //#region src/engines/arch-rules/matchers.ts
60
+ /** Extract imports from JS/TS content */
61
+ function extractJsImports(content) {
62
+ const imports = [];
63
+ const lines = content.split("\n");
64
+ for (let i = 0; i < lines.length; i++) {
65
+ const trimmed = lines[i].trim();
66
+ const lineNum = i + 1;
67
+ const staticMatch = trimmed.match(/^import\s+(?:type\s+)?(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/);
68
+ if (staticMatch) {
69
+ imports.push({
70
+ line: lineNum,
71
+ source: staticMatch[1],
72
+ raw: trimmed
73
+ });
74
+ continue;
75
+ }
76
+ const sideEffectMatch = trimmed.match(/^import\s+['"]([^'"]+)['"]/);
77
+ if (sideEffectMatch) {
78
+ imports.push({
79
+ line: lineNum,
80
+ source: sideEffectMatch[1],
81
+ raw: trimmed
82
+ });
83
+ continue;
84
+ }
85
+ const dynMatch = trimmed.match(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
86
+ if (dynMatch) {
87
+ imports.push({
88
+ line: lineNum,
89
+ source: dynMatch[1],
90
+ raw: trimmed
91
+ });
92
+ continue;
93
+ }
94
+ const reqMatch = trimmed.match(/(?:const|let|var)\s+[^=]*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
95
+ if (reqMatch) imports.push({
96
+ line: lineNum,
97
+ source: reqMatch[1],
98
+ raw: trimmed
99
+ });
100
+ }
101
+ return imports;
102
+ }
103
+ /** Extract imports from Python content */
104
+ function extractPythonImports(content) {
105
+ const imports = [];
106
+ const lines = content.split("\n");
107
+ for (let i = 0; i < lines.length; i++) {
108
+ const trimmed = lines[i].trim();
109
+ const lineNum = i + 1;
110
+ const fromMatch = trimmed.match(/^from\s+([^\s]+)\s+import/);
111
+ if (fromMatch) {
112
+ imports.push({
113
+ line: lineNum,
114
+ source: fromMatch[1],
115
+ raw: trimmed
116
+ });
117
+ continue;
118
+ }
119
+ const importMatch = trimmed.match(/^import\s+([^\s,]+)/);
120
+ if (importMatch) imports.push({
121
+ line: lineNum,
122
+ source: importMatch[1],
123
+ raw: trimmed
124
+ });
125
+ }
126
+ return imports;
127
+ }
128
+ /** Extract imports from Go content */
129
+ function extractGoImports(content) {
130
+ const imports = [];
131
+ const lines = content.split("\n");
132
+ for (let i = 0; i < lines.length; i++) {
133
+ const trimmed = lines[i].trim();
134
+ const lineNum = i + 1;
135
+ const singleMatch = trimmed.match(/^import\s+"([^"]+)"/);
136
+ if (singleMatch) {
137
+ imports.push({
138
+ line: lineNum,
139
+ source: singleMatch[1],
140
+ raw: trimmed
141
+ });
142
+ continue;
143
+ }
144
+ const multiMatch = trimmed.match(/^"([^"]+)"$/);
145
+ if (multiMatch) {
146
+ const nearby = content.substring(Math.max(0, content.indexOf(lines[i]) - 200), content.indexOf(lines[i]) + lines[i].length);
147
+ if (nearby.includes("import") && nearby.includes("(")) imports.push({
148
+ line: lineNum,
149
+ source: multiMatch[1],
150
+ raw: trimmed
151
+ });
152
+ }
153
+ }
154
+ return imports;
155
+ }
156
+ /** Detect language from file extension and extract imports */
157
+ function extractImportsFromContent(content, filePath) {
158
+ const ext = filePath.split(".").pop() ?? "";
159
+ if ([
160
+ "ts",
161
+ "tsx",
162
+ "js",
163
+ "jsx",
164
+ "mjs",
165
+ "cjs"
166
+ ].includes(ext)) return extractJsImports(content);
167
+ if (["py", "pyw"].includes(ext)) return extractPythonImports(content);
168
+ if (ext === "go") return extractGoImports(content);
169
+ return extractJsImports(content);
170
+ }
171
+ /** Check if a path matches a glob pattern */
172
+ function globMatch(pattern, path) {
173
+ return minimatch(path, pattern, { dot: true });
174
+ }
175
+ /** Build a diagnostic for an arch-rules violation */
176
+ function buildDiag(opts) {
177
+ return {
178
+ filePath: opts.filePath,
179
+ engine: "arch-rules",
180
+ rule: `arch-rules/${opts.rule.name}`,
181
+ severity: opts.rule.severity,
182
+ message: opts.message,
183
+ help: opts.help,
184
+ line: opts.line,
185
+ column: opts.column,
186
+ category: "architecture",
187
+ fixable: false,
188
+ detail: {
189
+ ruleName: opts.rule.name,
190
+ ruleType: opts.rule.type
191
+ }
192
+ };
193
+ }
194
+ /**
195
+ * forbid_import: Checks if a file contains a forbidden import.
196
+ * The `forbid` field is a glob that matches against import sources.
197
+ * The `match` field is a glob that selects which files to check.
198
+ */
199
+ function applyForbidImport(rule, content, filePath) {
200
+ if (!rule.forbid) return [];
201
+ if (!globMatch(rule.match, filePath)) return [];
202
+ if (rule.where && !globMatch(rule.where, filePath)) return [];
203
+ const imports = extractImportsFromContent(content, filePath);
204
+ const results = [];
205
+ for (const imp of imports) if (globMatch(rule.forbid, imp.source)) {
206
+ const col = imp.raw.indexOf(imp.source) + 1;
207
+ results.push(buildDiag({
208
+ filePath,
209
+ rule,
210
+ line: imp.line,
211
+ column: Math.max(col, 1),
212
+ message: `Forbidden import '${imp.source}' found (rule: ${rule.name})`,
213
+ help: `The rule "${rule.name}" forbids importing modules matching "${rule.forbid}". Remove this import or update the rule.`
214
+ }));
215
+ }
216
+ return results;
217
+ }
218
+ /**
219
+ * forbid_import_from_path: Checks cross-layer imports.
220
+ * Files matching `match` must not import from paths matching `from`.
221
+ * The `forbid` field specifies what is being imported (glob on import source).
222
+ * The `from` field specifies the source path pattern that triggers the violation.
223
+ */
224
+ function applyForbidImportFromPath(rule, content, filePath) {
225
+ if (!rule.forbid || !rule.from) return [];
226
+ if (!globMatch(rule.match, filePath)) return [];
227
+ if (rule.where && !globMatch(rule.where, filePath)) return [];
228
+ const imports = extractImportsFromContent(content, filePath);
229
+ const results = [];
230
+ for (const imp of imports) {
231
+ if (!globMatch(rule.forbid, imp.source)) continue;
232
+ if (!globMatch(rule.from, imp.source)) continue;
233
+ const col = imp.raw.indexOf(imp.source) + 1;
234
+ results.push(buildDiag({
235
+ filePath,
236
+ rule,
237
+ line: imp.line,
238
+ column: Math.max(col, 1),
239
+ message: `Cross-layer import: '${imp.source}' is forbidden from '${filePath}' (rule: ${rule.name})`,
240
+ help: `The rule "${rule.name}" forbids importing "${rule.forbid}" from "${rule.from}" in files matching "${rule.match}". Refactor to use a service layer or adapter instead.`
241
+ }));
242
+ }
243
+ return results;
244
+ }
245
+ /**
246
+ * require_pattern: Checks that a required regex pattern exists in the file.
247
+ * The `match` field is a glob that selects which files to check.
248
+ * The `pattern` field is a regex that must be present in the file content.
249
+ */
250
+ function applyRequirePattern(rule, content, filePath) {
251
+ if (!rule.pattern) return [];
252
+ if (!globMatch(rule.match, filePath)) return [];
253
+ if (rule.where && !globMatch(rule.where, filePath)) return [];
254
+ const results = [];
255
+ try {
256
+ if (!new RegExp(rule.pattern, "m").test(content)) results.push(buildDiag({
257
+ filePath,
258
+ rule,
259
+ line: 1,
260
+ column: 1,
261
+ message: `Required pattern /${rule.pattern}/ not found (rule: ${rule.name})`,
262
+ help: `The rule "${rule.name}" requires files matching "${rule.match}" to contain the pattern /${rule.pattern}/. Add the required pattern to this file.`
263
+ }));
264
+ } catch (err) {
265
+ results.push(buildDiag({
266
+ filePath,
267
+ rule,
268
+ line: 1,
269
+ column: 1,
270
+ message: `Rule "${rule.name}" has invalid regex pattern: ${rule.pattern}`,
271
+ help: `Fix the regex pattern in .deep-slop/rules.yml for rule "${rule.name}".`
272
+ }));
273
+ }
274
+ return results;
275
+ }
276
+ /**
277
+ * Apply a single rule to a file, dispatching to the correct matcher.
278
+ */
279
+ function applyRule(rule, content, filePath) {
280
+ switch (rule.type) {
281
+ case "forbid_import": return applyForbidImport(rule, content, filePath);
282
+ case "forbid_import_from_path": return applyForbidImportFromPath(rule, content, filePath);
283
+ case "require_pattern": return applyRequirePattern(rule, content, filePath);
284
+ default: return [];
285
+ }
286
+ }
287
+
288
+ //#endregion
289
+ //#region src/engines/arch-rules/index.ts
290
+ const archRulesEngine = {
291
+ name: "arch-rules",
292
+ description: "User-defined architecture rules from .deep-slop/rules.yml: forbid imports, enforce cross-layer boundaries, and require patterns.",
293
+ supportedLanguages: [
294
+ "typescript",
295
+ "javascript",
296
+ "python",
297
+ "go"
298
+ ],
299
+ async run(context) {
300
+ const start = performance.now();
301
+ const { rootDirectory, config } = context;
302
+ let rules;
303
+ try {
304
+ rules = await loadRules(rootDirectory);
305
+ } catch (err) {
306
+ return {
307
+ engine: this.name,
308
+ diagnostics: [],
309
+ elapsed: performance.now() - start,
310
+ skipped: true,
311
+ skipReason: `Failed to load rules: ${err instanceof Error ? err.message : String(err)}`
312
+ };
313
+ }
314
+ if (rules.length === 0) return {
315
+ engine: this.name,
316
+ diagnostics: [],
317
+ elapsed: performance.now() - start,
318
+ skipped: true,
319
+ skipReason: "No rules defined in .deep-slop/rules.yml"
320
+ };
321
+ const files = await collectFiles(rootDirectory, [
322
+ "typescript",
323
+ "javascript",
324
+ "python",
325
+ "go"
326
+ ], config.exclude, context.files);
327
+ if (files.length === 0) return {
328
+ engine: this.name,
329
+ diagnostics: [],
330
+ elapsed: performance.now() - start,
331
+ skipped: true,
332
+ skipReason: "No files found for supported languages (ts, js, py, go)"
333
+ };
334
+ const diagnostics = [];
335
+ for (const absPath of files) {
336
+ const relPath = relative(rootDirectory, absPath);
337
+ let content;
338
+ try {
339
+ content = await readFileContent(absPath);
340
+ } catch {
341
+ continue;
342
+ }
343
+ for (const rule of rules) {
344
+ const ruleDiagnostics = applyRule(rule, content, relPath);
345
+ diagnostics.push(...ruleDiagnostics);
346
+ }
347
+ }
348
+ return {
349
+ engine: this.name,
350
+ diagnostics,
351
+ elapsed: performance.now() - start,
352
+ skipped: false
353
+ };
354
+ }
355
+ };
356
+
357
+ //#endregion
358
+ export { archRulesEngine };