@oxog/codeguardian 1.0.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.
@@ -0,0 +1,1316 @@
1
+ import ts from 'typescript';
2
+
3
+ // src/graph/query.ts
4
+ function findCircularDeps(graph) {
5
+ const cycles = [];
6
+ const visited = /* @__PURE__ */ new Set();
7
+ const inStack = /* @__PURE__ */ new Set();
8
+ const stack = [];
9
+ const dfs = (node) => {
10
+ if (inStack.has(node)) {
11
+ const cycleStart = stack.indexOf(node);
12
+ if (cycleStart !== -1) {
13
+ const cycle = [...stack.slice(cycleStart), node];
14
+ cycles.push(cycle);
15
+ }
16
+ return;
17
+ }
18
+ if (visited.has(node)) return;
19
+ visited.add(node);
20
+ inStack.add(node);
21
+ stack.push(node);
22
+ const deps = graph.dependencies.adjacency.get(node);
23
+ if (deps) {
24
+ for (const dep of deps) {
25
+ dfs(dep);
26
+ }
27
+ }
28
+ stack.pop();
29
+ inStack.delete(node);
30
+ };
31
+ for (const node of graph.dependencies.adjacency.keys()) {
32
+ dfs(node);
33
+ }
34
+ return cycles;
35
+ }
36
+
37
+ // src/plugins/core/architecture.ts
38
+ function architecturePlugin(config = {}) {
39
+ const fullConfig = {
40
+ layers: ["controller", "service", "repository", "util"],
41
+ enforceDirection: true,
42
+ maxFileLines: 300,
43
+ maxFunctionLines: 50,
44
+ ...config
45
+ };
46
+ return {
47
+ name: "architecture",
48
+ version: "1.0.0",
49
+ install(kernel) {
50
+ kernel.registerRule({
51
+ name: "architecture/layer-violation",
52
+ severity: "error",
53
+ description: "Detects when a lower layer imports from a higher layer",
54
+ category: "architecture",
55
+ check(context) {
56
+ const findings = [];
57
+ const layers = fullConfig.layers;
58
+ if (!fullConfig.enforceDirection || layers.length === 0) return findings;
59
+ const fileLayer = context.file.layer;
60
+ const fileLayerIndex = layers.indexOf(fileLayer);
61
+ if (fileLayerIndex === -1) return findings;
62
+ for (const imp of context.file.imports) {
63
+ const targetFile = context.graph.files.get(resolveImport(context.file.path, imp.source));
64
+ if (!targetFile) continue;
65
+ const targetLayerIndex = layers.indexOf(targetFile.layer);
66
+ if (targetLayerIndex === -1) continue;
67
+ if (fileLayerIndex <= targetLayerIndex) ; else {
68
+ findings.push({
69
+ message: `${fileLayer} layer importing from ${targetFile.layer} layer (violates layer direction)`,
70
+ file: context.file.path,
71
+ line: 1,
72
+ column: 1,
73
+ fix: {
74
+ suggestion: `${capitalize(fileLayer)}s should not depend on ${targetFile.layer}s. Invert the dependency.`
75
+ }
76
+ });
77
+ }
78
+ }
79
+ return findings;
80
+ }
81
+ });
82
+ kernel.registerRule({
83
+ name: "architecture/circular-dependency",
84
+ severity: "error",
85
+ description: "Detects circular import chains",
86
+ category: "architecture",
87
+ check(context) {
88
+ const findings = [];
89
+ const cycles = findCircularDeps(context.graph);
90
+ for (const cycle of cycles) {
91
+ if (cycle.includes(context.file.path)) {
92
+ findings.push({
93
+ message: `Circular dependency detected: ${cycle.join(" \u2192 ")}`,
94
+ file: context.file.path,
95
+ line: 1,
96
+ column: 1,
97
+ fix: {
98
+ suggestion: "Break the cycle by extracting shared types or using dependency injection."
99
+ }
100
+ });
101
+ }
102
+ }
103
+ return findings;
104
+ }
105
+ });
106
+ kernel.registerRule({
107
+ name: "architecture/file-role-mismatch",
108
+ severity: "warning",
109
+ description: "Detects when file content does not match directory role",
110
+ category: "architecture",
111
+ check(context) {
112
+ const findings = [];
113
+ const path = context.file.path.toLowerCase();
114
+ const role = context.file.role;
115
+ if (path.includes("/service") && role !== "service" && role !== "unknown") {
116
+ findings.push({
117
+ message: `File is in services directory but detected role is "${role}"`,
118
+ file: context.file.path,
119
+ line: 1,
120
+ column: 1,
121
+ fix: {
122
+ suggestion: "Move this file to the appropriate directory or rename it."
123
+ }
124
+ });
125
+ }
126
+ if (path.includes("/controller") && role !== "controller" && role !== "unknown") {
127
+ findings.push({
128
+ message: `File is in controllers directory but detected role is "${role}"`,
129
+ file: context.file.path,
130
+ line: 1,
131
+ column: 1,
132
+ fix: {
133
+ suggestion: "Move this file to the appropriate directory or rename it."
134
+ }
135
+ });
136
+ }
137
+ return findings;
138
+ }
139
+ });
140
+ kernel.registerRule({
141
+ name: "architecture/god-file",
142
+ severity: "warning",
143
+ description: "Detects files exceeding maximum line count",
144
+ category: "architecture",
145
+ check(context) {
146
+ const maxLines = fullConfig.maxFileLines ?? 300;
147
+ if (context.file.loc > maxLines) {
148
+ return [
149
+ {
150
+ message: `File has ${context.file.loc} lines (max: ${maxLines})`,
151
+ file: context.file.path,
152
+ line: 1,
153
+ column: 1,
154
+ fix: {
155
+ suggestion: "Split this file into smaller, focused modules."
156
+ }
157
+ }
158
+ ];
159
+ }
160
+ return [];
161
+ }
162
+ });
163
+ kernel.registerRule({
164
+ name: "architecture/god-function",
165
+ severity: "warning",
166
+ description: "Detects functions exceeding maximum line count",
167
+ category: "architecture",
168
+ check(context) {
169
+ const findings = [];
170
+ const maxLines = fullConfig.maxFunctionLines ?? 50;
171
+ for (const fn of context.file.functions) {
172
+ const fnLines = fn.endLine - fn.startLine + 1;
173
+ if (fnLines > maxLines) {
174
+ findings.push({
175
+ message: `Function "${fn.name}" has ${fnLines} lines (max: ${maxLines})`,
176
+ file: context.file.path,
177
+ line: fn.startLine,
178
+ column: 1,
179
+ fix: {
180
+ suggestion: "Extract logic into smaller helper functions."
181
+ }
182
+ });
183
+ }
184
+ }
185
+ return findings;
186
+ }
187
+ });
188
+ kernel.registerRule({
189
+ name: "architecture/barrel-explosion",
190
+ severity: "info",
191
+ description: "Detects barrel files (index.ts) that re-export everything",
192
+ category: "architecture",
193
+ check(context) {
194
+ const findings = [];
195
+ const fileName = context.file.path.split("/").pop() ?? "";
196
+ if (fileName === "index.ts" || fileName === "index.tsx") {
197
+ const exportCount = context.file.exports.length;
198
+ if (exportCount > 10) {
199
+ findings.push({
200
+ message: `Barrel file re-exports ${exportCount} symbols (may cause bundle size issues)`,
201
+ file: context.file.path,
202
+ line: 1,
203
+ column: 1,
204
+ fix: {
205
+ suggestion: "Consider using direct imports instead of barrel files for tree-shaking."
206
+ }
207
+ });
208
+ }
209
+ }
210
+ return findings;
211
+ }
212
+ });
213
+ }
214
+ };
215
+ }
216
+ function resolveImport(fromFile, source) {
217
+ if (!source.startsWith(".")) return source;
218
+ const fromDir = fromFile.split("/").slice(0, -1).join("/");
219
+ const parts = [...fromDir.split("/"), ...source.split("/")];
220
+ const resolved = [];
221
+ for (const part of parts) {
222
+ if (part === "..") resolved.pop();
223
+ else if (part !== ".") resolved.push(part);
224
+ }
225
+ let result = resolved.join("/");
226
+ if (!result.endsWith(".ts") && !result.endsWith(".tsx")) {
227
+ result += ".ts";
228
+ }
229
+ return result;
230
+ }
231
+ function capitalize(str) {
232
+ return str.charAt(0).toUpperCase() + str.slice(1);
233
+ }
234
+ var DB_METHODS = ["query", "execute", "raw", "prepare", "exec"];
235
+ var SECRET_PATTERNS = [
236
+ /^(sk_|pk_|api_|token_|secret_|password|auth_)/i,
237
+ /^(AKIA[0-9A-Z]{16})/,
238
+ // AWS access key
239
+ /^eyJ[A-Za-z0-9-_]+\.eyJ/,
240
+ // JWT token
241
+ /^(ghp_|gho_|ghu_|ghs_|ghr_)/,
242
+ // GitHub tokens
243
+ /(mongodb(\+srv)?:\/\/|postgres(ql)?:\/\/|mysql:\/\/|redis:\/\/)/i
244
+ // Connection strings
245
+ ];
246
+ function securityPlugin(config = {}) {
247
+ const fullConfig = {
248
+ checkInjection: true,
249
+ checkAuth: true,
250
+ checkSecrets: true,
251
+ checkXSS: true,
252
+ ...config
253
+ };
254
+ return {
255
+ name: "security",
256
+ version: "1.0.0",
257
+ install(kernel) {
258
+ kernel.registerRule({
259
+ name: "security/sql-injection",
260
+ severity: "critical",
261
+ description: "Detects string concatenation in SQL queries",
262
+ category: "security",
263
+ check(context) {
264
+ if (!fullConfig.checkInjection) return [];
265
+ const findings = [];
266
+ context.walk(context.ast, {
267
+ CallExpression(node) {
268
+ const call = node;
269
+ const expr = call.expression;
270
+ let methodName = "";
271
+ if (ts.isPropertyAccessExpression(expr)) {
272
+ methodName = expr.name.text;
273
+ } else if (ts.isIdentifier(expr)) {
274
+ methodName = expr.text;
275
+ }
276
+ if (DB_METHODS.includes(methodName)) {
277
+ for (const arg of call.arguments) {
278
+ if (ts.isTemplateExpression(arg) || context.hasStringConcat(arg)) {
279
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
280
+ findings.push({
281
+ message: "Raw string concatenation in SQL query \u2014 potential SQL injection",
282
+ file: context.file.path,
283
+ line: pos.line + 1,
284
+ column: pos.character + 1,
285
+ fix: {
286
+ suggestion: "Use parameterized queries instead of string templates."
287
+ }
288
+ });
289
+ }
290
+ }
291
+ }
292
+ }
293
+ });
294
+ return findings;
295
+ }
296
+ });
297
+ kernel.registerRule({
298
+ name: "security/hardcoded-secret",
299
+ severity: "critical",
300
+ description: "Detects hardcoded API keys, tokens, and passwords",
301
+ category: "security",
302
+ check(context) {
303
+ if (!fullConfig.checkSecrets) return [];
304
+ if (context.file.role === "test") return [];
305
+ const findings = [];
306
+ context.walk(context.ast, {
307
+ StringLiteral(node) {
308
+ const str = node;
309
+ const value = str.text;
310
+ if (value.length < 8) return;
311
+ for (const pattern of SECRET_PATTERNS) {
312
+ if (pattern.test(value)) {
313
+ const pos = context.ast.getLineAndCharacterOfPosition(str.getStart(context.ast));
314
+ findings.push({
315
+ message: "Possible hardcoded secret detected",
316
+ file: context.file.path,
317
+ line: pos.line + 1,
318
+ column: pos.character + 1,
319
+ fix: {
320
+ suggestion: "Move secrets to environment variables or a secure vault."
321
+ }
322
+ });
323
+ break;
324
+ }
325
+ }
326
+ },
327
+ NoSubstitutionTemplateLiteral(node) {
328
+ const tmpl = node;
329
+ const value = tmpl.text;
330
+ if (value.length < 8) return;
331
+ for (const pattern of SECRET_PATTERNS) {
332
+ if (pattern.test(value)) {
333
+ const pos = context.ast.getLineAndCharacterOfPosition(tmpl.getStart(context.ast));
334
+ findings.push({
335
+ message: "Possible hardcoded secret detected",
336
+ file: context.file.path,
337
+ line: pos.line + 1,
338
+ column: pos.character + 1,
339
+ fix: {
340
+ suggestion: "Move secrets to environment variables or a secure vault."
341
+ }
342
+ });
343
+ break;
344
+ }
345
+ }
346
+ }
347
+ });
348
+ return findings;
349
+ }
350
+ });
351
+ kernel.registerRule({
352
+ name: "security/eval-usage",
353
+ severity: "critical",
354
+ description: "Detects eval(), Function(), and similar unsafe patterns",
355
+ category: "security",
356
+ check(context) {
357
+ const findings = [];
358
+ context.walk(context.ast, {
359
+ CallExpression(node) {
360
+ const call = node;
361
+ let name = "";
362
+ if (ts.isIdentifier(call.expression)) {
363
+ name = call.expression.text;
364
+ }
365
+ if (name === "eval" || name === "Function") {
366
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
367
+ findings.push({
368
+ message: `Unsafe ${name}() call \u2014 potential code injection`,
369
+ file: context.file.path,
370
+ line: pos.line + 1,
371
+ column: pos.character + 1,
372
+ fix: {
373
+ suggestion: `Avoid ${name}(). Use safe alternatives like JSON.parse() or structured data.`
374
+ }
375
+ });
376
+ }
377
+ if ((name === "setTimeout" || name === "setInterval") && call.arguments.length > 0) {
378
+ const firstArg = call.arguments[0];
379
+ if (ts.isStringLiteral(firstArg)) {
380
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
381
+ findings.push({
382
+ message: `${name}() with string argument is equivalent to eval()`,
383
+ file: context.file.path,
384
+ line: pos.line + 1,
385
+ column: pos.character + 1,
386
+ fix: {
387
+ suggestion: "Pass a function reference instead of a string."
388
+ }
389
+ });
390
+ }
391
+ }
392
+ },
393
+ NewExpression(node) {
394
+ const newExpr = node;
395
+ if (ts.isIdentifier(newExpr.expression) && newExpr.expression.text === "Function") {
396
+ const pos = context.ast.getLineAndCharacterOfPosition(newExpr.getStart(context.ast));
397
+ findings.push({
398
+ message: "new Function() is equivalent to eval() \u2014 potential code injection",
399
+ file: context.file.path,
400
+ line: pos.line + 1,
401
+ column: pos.character + 1,
402
+ fix: {
403
+ suggestion: "Avoid new Function(). Use safe alternatives."
404
+ }
405
+ });
406
+ }
407
+ }
408
+ });
409
+ return findings;
410
+ }
411
+ });
412
+ kernel.registerRule({
413
+ name: "security/prototype-pollution",
414
+ severity: "error",
415
+ description: "Detects potential prototype pollution",
416
+ category: "security",
417
+ check(context) {
418
+ const findings = [];
419
+ context.walk(context.ast, {
420
+ PropertyAccessExpression(node) {
421
+ const propAccess = node;
422
+ if (propAccess.name.text === "__proto__" || propAccess.name.text === "prototype") {
423
+ const parent = propAccess.parent;
424
+ if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
425
+ const pos = context.ast.getLineAndCharacterOfPosition(propAccess.getStart(context.ast));
426
+ findings.push({
427
+ message: "Direct prototype assignment \u2014 potential prototype pollution",
428
+ file: context.file.path,
429
+ line: pos.line + 1,
430
+ column: pos.character + 1,
431
+ fix: {
432
+ suggestion: "Use Object.create(null) or Map for dynamic key-value stores."
433
+ }
434
+ });
435
+ }
436
+ }
437
+ }
438
+ });
439
+ return findings;
440
+ }
441
+ });
442
+ kernel.registerRule({
443
+ name: "security/xss-risk",
444
+ severity: "error",
445
+ description: "Detects innerHTML and similar XSS-prone patterns",
446
+ category: "security",
447
+ check(context) {
448
+ if (!fullConfig.checkXSS) return [];
449
+ const findings = [];
450
+ const xssProps = ["innerHTML", "outerHTML", "dangerouslySetInnerHTML"];
451
+ context.walk(context.ast, {
452
+ PropertyAccessExpression(node) {
453
+ const propAccess = node;
454
+ if (xssProps.includes(propAccess.name.text)) {
455
+ const pos = context.ast.getLineAndCharacterOfPosition(propAccess.getStart(context.ast));
456
+ findings.push({
457
+ message: `Use of ${propAccess.name.text} \u2014 potential XSS vulnerability`,
458
+ file: context.file.path,
459
+ line: pos.line + 1,
460
+ column: pos.character + 1,
461
+ fix: {
462
+ suggestion: "Sanitize HTML content before insertion or use safe alternatives."
463
+ }
464
+ });
465
+ }
466
+ },
467
+ CallExpression(node) {
468
+ const call = node;
469
+ if (ts.isPropertyAccessExpression(call.expression)) {
470
+ const name = call.expression.name.text;
471
+ if (name === "write" || name === "writeln") {
472
+ if (ts.isIdentifier(call.expression.expression) && call.expression.expression.text === "document") {
473
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
474
+ findings.push({
475
+ message: "document.write() is an XSS risk",
476
+ file: context.file.path,
477
+ line: pos.line + 1,
478
+ column: pos.character + 1,
479
+ fix: {
480
+ suggestion: "Use DOM manipulation methods instead of document.write()."
481
+ }
482
+ });
483
+ }
484
+ }
485
+ }
486
+ }
487
+ });
488
+ return findings;
489
+ }
490
+ });
491
+ kernel.registerRule({
492
+ name: "security/missing-auth-check",
493
+ severity: "warning",
494
+ description: "Detects route handlers without auth checks",
495
+ category: "security",
496
+ check(context) {
497
+ if (!fullConfig.checkAuth) return [];
498
+ if (context.file.role !== "controller") return [];
499
+ const findings = [];
500
+ const fileText = context.ast.getFullText();
501
+ const hasAuthReference = fileText.includes("auth") || fileText.includes("Auth") || fileText.includes("authenticate") || fileText.includes("authorize") || fileText.includes("guard") || fileText.includes("middleware") || fileText.includes("jwt") || fileText.includes("token");
502
+ if (!hasAuthReference && context.file.functions.length > 0) {
503
+ findings.push({
504
+ message: "Controller has no authentication/authorization references",
505
+ file: context.file.path,
506
+ line: 1,
507
+ column: 1,
508
+ fix: {
509
+ suggestion: "Add authentication middleware or auth checks to route handlers."
510
+ }
511
+ });
512
+ }
513
+ return findings;
514
+ }
515
+ });
516
+ kernel.registerRule({
517
+ name: "security/insecure-random",
518
+ severity: "warning",
519
+ description: "Detects Math.random() in security-sensitive contexts",
520
+ category: "security",
521
+ check(context) {
522
+ const findings = [];
523
+ context.walk(context.ast, {
524
+ CallExpression(node) {
525
+ const call = node;
526
+ if (ts.isPropertyAccessExpression(call.expression)) {
527
+ if (ts.isIdentifier(call.expression.expression) && call.expression.expression.text === "Math" && call.expression.name.text === "random") {
528
+ const fileText = context.ast.getFullText();
529
+ const isSecurityContext = fileText.includes("token") || fileText.includes("secret") || fileText.includes("password") || fileText.includes("hash") || fileText.includes("crypto") || fileText.includes("session");
530
+ if (isSecurityContext) {
531
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
532
+ findings.push({
533
+ message: "Math.random() is not cryptographically secure",
534
+ file: context.file.path,
535
+ line: pos.line + 1,
536
+ column: pos.character + 1,
537
+ fix: {
538
+ suggestion: "Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive values."
539
+ }
540
+ });
541
+ }
542
+ }
543
+ }
544
+ }
545
+ });
546
+ return findings;
547
+ }
548
+ });
549
+ kernel.registerRule({
550
+ name: "security/path-traversal",
551
+ severity: "error",
552
+ description: "Detects file operations with potential path traversal",
553
+ category: "security",
554
+ check(context) {
555
+ const findings = [];
556
+ const fsOps = ["readFile", "readFileSync", "writeFile", "writeFileSync", "createReadStream", "createWriteStream", "access", "open"];
557
+ context.walk(context.ast, {
558
+ CallExpression(node) {
559
+ const call = node;
560
+ let methodName = "";
561
+ if (ts.isPropertyAccessExpression(call.expression)) {
562
+ methodName = call.expression.name.text;
563
+ } else if (ts.isIdentifier(call.expression)) {
564
+ methodName = call.expression.text;
565
+ }
566
+ if (fsOps.includes(methodName) && call.arguments.length > 0) {
567
+ const firstArg = call.arguments[0];
568
+ if (ts.isTemplateExpression(firstArg) || context.hasStringConcat(firstArg)) {
569
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
570
+ findings.push({
571
+ message: `File operation "${methodName}" with dynamic path \u2014 potential path traversal`,
572
+ file: context.file.path,
573
+ line: pos.line + 1,
574
+ column: pos.character + 1,
575
+ fix: {
576
+ suggestion: "Validate and sanitize file paths. Use path.resolve() and check against a whitelist."
577
+ }
578
+ });
579
+ }
580
+ }
581
+ }
582
+ });
583
+ return findings;
584
+ }
585
+ });
586
+ }
587
+ };
588
+ }
589
+ var DB_CALL_PATTERNS = ["find", "findOne", "findAll", "findById", "query", "execute", "fetch", "get", "select"];
590
+ var SYNC_FS_METHODS = ["readFileSync", "writeFileSync", "appendFileSync", "mkdirSync", "readdirSync", "statSync", "existsSync", "unlinkSync", "copyFileSync"];
591
+ function performancePlugin(config = {}) {
592
+ const fullConfig = {
593
+ checkN1Queries: true,
594
+ checkMemoryLeaks: true,
595
+ checkAsyncPatterns: true,
596
+ checkBundleSize: false,
597
+ ...config
598
+ };
599
+ return {
600
+ name: "performance",
601
+ version: "1.0.0",
602
+ install(kernel) {
603
+ kernel.registerRule({
604
+ name: "performance/n1-query",
605
+ severity: "warning",
606
+ description: "Detects database calls inside loops (potential N+1 query)",
607
+ category: "performance",
608
+ check(context) {
609
+ if (!fullConfig.checkN1Queries) return [];
610
+ const findings = [];
611
+ const checkForDbCallsInLoop = (loopNode) => {
612
+ ts.forEachChild(loopNode, function visitChild(child) {
613
+ if (ts.isCallExpression(child)) {
614
+ let methodName = "";
615
+ if (ts.isPropertyAccessExpression(child.expression)) {
616
+ methodName = child.expression.name.text;
617
+ }
618
+ if (DB_CALL_PATTERNS.includes(methodName)) {
619
+ const pos = context.ast.getLineAndCharacterOfPosition(child.getStart(context.ast));
620
+ findings.push({
621
+ message: `Potential N+1 query: "${methodName}" called inside a loop`,
622
+ file: context.file.path,
623
+ line: pos.line + 1,
624
+ column: pos.character + 1,
625
+ fix: {
626
+ suggestion: "Batch queries using Promise.all() or a single query with IN clause."
627
+ }
628
+ });
629
+ }
630
+ }
631
+ ts.forEachChild(child, visitChild);
632
+ });
633
+ };
634
+ context.walk(context.ast, {
635
+ ForStatement(node) {
636
+ checkForDbCallsInLoop(node);
637
+ },
638
+ ForInStatement(node) {
639
+ checkForDbCallsInLoop(node);
640
+ },
641
+ ForOfStatement(node) {
642
+ checkForDbCallsInLoop(node);
643
+ },
644
+ WhileStatement(node) {
645
+ checkForDbCallsInLoop(node);
646
+ },
647
+ DoStatement(node) {
648
+ checkForDbCallsInLoop(node);
649
+ }
650
+ });
651
+ context.walk(context.ast, {
652
+ CallExpression(node) {
653
+ const call = node;
654
+ if (ts.isPropertyAccessExpression(call.expression)) {
655
+ const name = call.expression.name.text;
656
+ if (name === "forEach" || name === "map") {
657
+ if (call.arguments.length > 0) {
658
+ checkForDbCallsInLoop(call.arguments[0]);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ });
664
+ return findings;
665
+ }
666
+ });
667
+ kernel.registerRule({
668
+ name: "performance/sync-in-async",
669
+ severity: "warning",
670
+ description: "Detects synchronous file operations in async functions",
671
+ category: "performance",
672
+ check(context) {
673
+ if (!fullConfig.checkAsyncPatterns) return [];
674
+ const findings = [];
675
+ for (const fn of context.file.functions) {
676
+ if (!fn.isAsync) continue;
677
+ context.walk(context.ast, {
678
+ CallExpression(node) {
679
+ const call = node;
680
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
681
+ const callLine = pos.line + 1;
682
+ if (callLine < fn.startLine || callLine > fn.endLine) return;
683
+ let methodName = "";
684
+ if (ts.isPropertyAccessExpression(call.expression)) {
685
+ methodName = call.expression.name.text;
686
+ } else if (ts.isIdentifier(call.expression)) {
687
+ methodName = call.expression.text;
688
+ }
689
+ if (SYNC_FS_METHODS.includes(methodName)) {
690
+ findings.push({
691
+ message: `Synchronous "${methodName}" in async function "${fn.name}"`,
692
+ file: context.file.path,
693
+ line: callLine,
694
+ column: pos.character + 1,
695
+ fix: {
696
+ suggestion: `Use the async version instead (e.g., fs.promises.${methodName.replace("Sync", "")}).`
697
+ }
698
+ });
699
+ }
700
+ }
701
+ });
702
+ }
703
+ return findings;
704
+ }
705
+ });
706
+ kernel.registerRule({
707
+ name: "performance/memory-leak-risk",
708
+ severity: "warning",
709
+ description: "Detects addEventListener without removeEventListener, setInterval without clearInterval",
710
+ category: "performance",
711
+ check(context) {
712
+ if (!fullConfig.checkMemoryLeaks) return [];
713
+ const findings = [];
714
+ const fileText = context.ast.getFullText();
715
+ if (fileText.includes("addEventListener") && !fileText.includes("removeEventListener")) {
716
+ context.walk(context.ast, {
717
+ CallExpression(node) {
718
+ const call = node;
719
+ if (ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === "addEventListener") {
720
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
721
+ findings.push({
722
+ message: "addEventListener without corresponding removeEventListener \u2014 potential memory leak",
723
+ file: context.file.path,
724
+ line: pos.line + 1,
725
+ column: pos.character + 1,
726
+ fix: {
727
+ suggestion: "Add a cleanup function that calls removeEventListener."
728
+ }
729
+ });
730
+ }
731
+ }
732
+ });
733
+ }
734
+ if (fileText.includes("setInterval") && !fileText.includes("clearInterval")) {
735
+ context.walk(context.ast, {
736
+ CallExpression(node) {
737
+ const call = node;
738
+ if (ts.isIdentifier(call.expression) && call.expression.text === "setInterval") {
739
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
740
+ findings.push({
741
+ message: "setInterval without clearInterval \u2014 potential memory leak",
742
+ file: context.file.path,
743
+ line: pos.line + 1,
744
+ column: pos.character + 1,
745
+ fix: {
746
+ suggestion: "Store the interval ID and clear it when no longer needed."
747
+ }
748
+ });
749
+ }
750
+ }
751
+ });
752
+ }
753
+ return findings;
754
+ }
755
+ });
756
+ kernel.registerRule({
757
+ name: "performance/unbounded-query",
758
+ severity: "warning",
759
+ description: "Detects database queries without LIMIT or pagination",
760
+ category: "performance",
761
+ check(context) {
762
+ const findings = [];
763
+ context.walk(context.ast, {
764
+ CallExpression(node) {
765
+ const call = node;
766
+ let methodName = "";
767
+ if (ts.isPropertyAccessExpression(call.expression)) {
768
+ methodName = call.expression.name.text;
769
+ }
770
+ if (methodName === "findAll" || methodName === "find") {
771
+ const argText = call.arguments.map((a) => a.getText(context.ast)).join(" ");
772
+ if (!argText.includes("limit") && !argText.includes("take") && !argText.includes("LIMIT")) {
773
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
774
+ findings.push({
775
+ message: `"${methodName}" without limit \u2014 could return unbounded results`,
776
+ file: context.file.path,
777
+ line: pos.line + 1,
778
+ column: pos.character + 1,
779
+ fix: {
780
+ suggestion: "Add a LIMIT clause or pagination to prevent loading too many records."
781
+ }
782
+ });
783
+ }
784
+ }
785
+ }
786
+ });
787
+ return findings;
788
+ }
789
+ });
790
+ kernel.registerRule({
791
+ name: "performance/missing-index-hint",
792
+ severity: "info",
793
+ description: "Detects queries that may need database indexes",
794
+ category: "performance",
795
+ check(_context) {
796
+ return [];
797
+ }
798
+ });
799
+ kernel.registerRule({
800
+ name: "performance/heavy-import",
801
+ severity: "info",
802
+ description: "Detects importing entire libraries when a smaller import is available",
803
+ category: "performance",
804
+ check(context) {
805
+ if (!fullConfig.checkBundleSize) return [];
806
+ const findings = [];
807
+ const heavyLibs = ["lodash", "moment", "rxjs"];
808
+ for (const imp of context.file.imports) {
809
+ for (const lib of heavyLibs) {
810
+ if (imp.source === lib && imp.specifiers.length <= 2) {
811
+ findings.push({
812
+ /* v8 ignore next */
813
+ message: `Importing from "${lib}" \u2014 consider using "${lib}/${imp.specifiers[0] ?? ""}" for smaller bundle`,
814
+ file: context.file.path,
815
+ line: 1,
816
+ column: 1,
817
+ fix: {
818
+ suggestion: `Use deep imports (e.g., "${lib}/functionName") for tree-shaking.`
819
+ }
820
+ });
821
+ }
822
+ }
823
+ }
824
+ return findings;
825
+ }
826
+ });
827
+ kernel.registerRule({
828
+ name: "performance/blocking-operation",
829
+ severity: "warning",
830
+ description: "Detects CPU-intensive operations in request handlers",
831
+ category: "performance",
832
+ check(context) {
833
+ if (context.file.role !== "controller") return [];
834
+ const findings = [];
835
+ context.walk(context.ast, {
836
+ CallExpression(node) {
837
+ const call = node;
838
+ let methodName = "";
839
+ if (ts.isPropertyAccessExpression(call.expression)) {
840
+ methodName = call.expression.name.text;
841
+ } else if (ts.isIdentifier(call.expression)) {
842
+ methodName = call.expression.text;
843
+ }
844
+ if (methodName === "parse" && ts.isPropertyAccessExpression(call.expression)) {
845
+ if (ts.isIdentifier(call.expression.expression) && call.expression.expression.text === "JSON") {
846
+ const pos = context.ast.getLineAndCharacterOfPosition(call.getStart(context.ast));
847
+ findings.push({
848
+ message: "JSON.parse() in request handler may block event loop for large payloads",
849
+ file: context.file.path,
850
+ line: pos.line + 1,
851
+ column: pos.character + 1,
852
+ fix: {
853
+ suggestion: "Consider streaming JSON parsing or limiting request body size."
854
+ }
855
+ });
856
+ }
857
+ }
858
+ }
859
+ });
860
+ return findings;
861
+ }
862
+ });
863
+ }
864
+ };
865
+ }
866
+ function qualityPlugin(config = {}) {
867
+ const fullConfig = {
868
+ checkDeadCode: true,
869
+ checkNaming: true,
870
+ checkComplexity: true,
871
+ maxCyclomaticComplexity: 15,
872
+ ...config
873
+ };
874
+ return {
875
+ name: "quality",
876
+ version: "1.0.0",
877
+ install(kernel) {
878
+ kernel.registerRule({
879
+ name: "quality/cyclomatic-complexity",
880
+ severity: "warning",
881
+ description: "Detects functions with high cyclomatic complexity",
882
+ category: "quality",
883
+ check(context) {
884
+ if (!fullConfig.checkComplexity) return [];
885
+ const findings = [];
886
+ const maxComplexity = fullConfig.maxCyclomaticComplexity ?? 15;
887
+ for (const fn of context.file.functions) {
888
+ if (fn.complexity > maxComplexity) {
889
+ findings.push({
890
+ message: `Function "${fn.name}" has cyclomatic complexity ${fn.complexity} (max: ${maxComplexity})`,
891
+ file: context.file.path,
892
+ line: fn.startLine,
893
+ column: 1,
894
+ fix: {
895
+ suggestion: "Simplify by extracting conditions into named functions or using early returns."
896
+ }
897
+ });
898
+ }
899
+ }
900
+ return findings;
901
+ }
902
+ });
903
+ kernel.registerRule({
904
+ name: "quality/dead-code",
905
+ severity: "warning",
906
+ description: "Detects exported symbols never imported by other files",
907
+ category: "quality",
908
+ check(context) {
909
+ if (!fullConfig.checkDeadCode) return [];
910
+ if (context.file.role === "test") return [];
911
+ const findings = [];
912
+ for (const exportName of context.file.exports) {
913
+ if (exportName === "default") continue;
914
+ const fileName = context.file.path.split("/").pop() ?? "";
915
+ if (fileName === "index.ts" || fileName === "index.tsx") continue;
916
+ const isUsed = context.isExternallyUsed(exportName);
917
+ if (!isUsed) {
918
+ findings.push({
919
+ message: `Exported symbol "${exportName}" is never imported by other files`,
920
+ file: context.file.path,
921
+ line: 1,
922
+ column: 1,
923
+ fix: {
924
+ suggestion: "Remove unused export or add an import if intentional."
925
+ }
926
+ });
927
+ }
928
+ }
929
+ return findings;
930
+ }
931
+ });
932
+ kernel.registerRule({
933
+ name: "quality/any-type",
934
+ severity: "warning",
935
+ description: "Detects usage of `any` type annotation",
936
+ category: "quality",
937
+ check(context) {
938
+ const findings = [];
939
+ context.walk(context.ast, {
940
+ // TypeReference for explicit 'any' annotations
941
+ AnyKeyword(node) {
942
+ const pos = context.ast.getLineAndCharacterOfPosition(node.getStart(context.ast));
943
+ findings.push({
944
+ message: "Usage of `any` type \u2014 use a specific type instead",
945
+ file: context.file.path,
946
+ line: pos.line + 1,
947
+ column: pos.character + 1,
948
+ fix: {
949
+ suggestion: "Replace `any` with a specific type or `unknown`."
950
+ }
951
+ });
952
+ }
953
+ });
954
+ return findings;
955
+ }
956
+ });
957
+ kernel.registerRule({
958
+ name: "quality/no-error-handling",
959
+ severity: "warning",
960
+ description: "Detects async functions without error handling",
961
+ category: "quality",
962
+ check(context) {
963
+ const findings = [];
964
+ for (const fn of context.file.functions) {
965
+ if (!fn.isAsync) continue;
966
+ const fileText = context.ast.getFullText();
967
+ const fnText = fileText.slice(
968
+ getLineOffset(fileText, fn.startLine),
969
+ getLineOffset(fileText, fn.endLine + 1)
970
+ );
971
+ const hasTryCatch = fnText.includes("try") && fnText.includes("catch");
972
+ const hasDotCatch = fnText.includes(".catch(");
973
+ const hasThrow = fnText.includes("throw ");
974
+ if (!hasTryCatch && !hasDotCatch && !hasThrow) {
975
+ findings.push({
976
+ message: `Async function "${fn.name}" has no error handling (try-catch or .catch())`,
977
+ file: context.file.path,
978
+ line: fn.startLine,
979
+ column: 1,
980
+ fix: {
981
+ suggestion: "Add try-catch around async operations or use .catch() on promises."
982
+ }
983
+ });
984
+ }
985
+ }
986
+ return findings;
987
+ }
988
+ });
989
+ kernel.registerRule({
990
+ name: "quality/inconsistent-naming",
991
+ severity: "info",
992
+ description: "Detects naming that does not follow project conventions",
993
+ category: "quality",
994
+ check(context) {
995
+ if (!fullConfig.checkNaming) return [];
996
+ const findings = [];
997
+ for (const fn of context.file.functions) {
998
+ if (fn.name && !/^[a-z_$]/.test(fn.name) && !/^[A-Z][A-Z_]+$/.test(fn.name)) {
999
+ if (context.file.role !== "unknown") {
1000
+ findings.push({
1001
+ message: `Function "${fn.name}" does not follow camelCase naming convention`,
1002
+ file: context.file.path,
1003
+ line: fn.startLine,
1004
+ column: 1,
1005
+ fix: {
1006
+ suggestion: "Use camelCase for function names (e.g., getUser, handleRequest)."
1007
+ }
1008
+ });
1009
+ }
1010
+ }
1011
+ }
1012
+ return findings;
1013
+ }
1014
+ });
1015
+ kernel.registerRule({
1016
+ name: "quality/magic-number",
1017
+ severity: "info",
1018
+ description: "Detects numeric literals used directly in logic",
1019
+ category: "quality",
1020
+ check(context) {
1021
+ const findings = [];
1022
+ const allowedNumbers = /* @__PURE__ */ new Set([0, 1, -1, 2, 100, 200, 201, 204, 301, 302, 400, 401, 403, 404, 500]);
1023
+ context.walk(context.ast, {
1024
+ NumericLiteral(node) {
1025
+ const num = node;
1026
+ const value = parseFloat(num.text);
1027
+ if (allowedNumbers.has(value)) return;
1028
+ let parent = num.parent;
1029
+ while (parent) {
1030
+ if (ts.isVariableDeclaration(parent) || ts.isEnumMember(parent) || ts.isPropertyAssignment(parent)) {
1031
+ return;
1032
+ }
1033
+ parent = parent.parent;
1034
+ }
1035
+ const pos = context.ast.getLineAndCharacterOfPosition(num.getStart(context.ast));
1036
+ findings.push({
1037
+ message: `Magic number ${num.text} \u2014 extract to a named constant`,
1038
+ file: context.file.path,
1039
+ line: pos.line + 1,
1040
+ column: pos.character + 1,
1041
+ fix: {
1042
+ suggestion: "Extract numeric literal to a named constant for clarity."
1043
+ }
1044
+ });
1045
+ }
1046
+ });
1047
+ return findings;
1048
+ }
1049
+ });
1050
+ kernel.registerRule({
1051
+ name: "quality/empty-catch",
1052
+ severity: "warning",
1053
+ description: "Detects empty catch blocks",
1054
+ category: "quality",
1055
+ check(context) {
1056
+ const findings = [];
1057
+ context.walk(context.ast, {
1058
+ CatchClause(node) {
1059
+ const catchClause = node;
1060
+ const block = catchClause.block;
1061
+ if (block.statements.length === 0) {
1062
+ const pos = context.ast.getLineAndCharacterOfPosition(catchClause.getStart(context.ast));
1063
+ findings.push({
1064
+ message: "Empty catch block \u2014 errors are silently swallowed",
1065
+ file: context.file.path,
1066
+ line: pos.line + 1,
1067
+ column: pos.character + 1,
1068
+ fix: {
1069
+ suggestion: "Handle the error, log it, or re-throw if appropriate."
1070
+ }
1071
+ });
1072
+ }
1073
+ }
1074
+ });
1075
+ return findings;
1076
+ }
1077
+ });
1078
+ kernel.registerRule({
1079
+ name: "quality/nested-callbacks",
1080
+ severity: "warning",
1081
+ description: "Detects deeply nested callbacks (> 3 levels)",
1082
+ category: "quality",
1083
+ check(context) {
1084
+ const findings = [];
1085
+ const maxNesting = 3;
1086
+ const checkNesting = (node, depth) => {
1087
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
1088
+ if (depth > maxNesting) {
1089
+ const pos = context.ast.getLineAndCharacterOfPosition(node.getStart(context.ast));
1090
+ findings.push({
1091
+ message: `Callback nested ${depth} levels deep (max: ${maxNesting})`,
1092
+ file: context.file.path,
1093
+ line: pos.line + 1,
1094
+ column: pos.character + 1,
1095
+ fix: {
1096
+ suggestion: "Use async/await or extract nested callbacks into named functions."
1097
+ }
1098
+ });
1099
+ return;
1100
+ }
1101
+ ts.forEachChild(node, (child) => checkNesting(child, depth + 1));
1102
+ } else {
1103
+ ts.forEachChild(node, (child) => checkNesting(child, depth));
1104
+ }
1105
+ };
1106
+ ts.forEachChild(context.ast, (child) => checkNesting(child, 0));
1107
+ return findings;
1108
+ }
1109
+ });
1110
+ }
1111
+ };
1112
+ }
1113
+ function getLineOffset(text, line) {
1114
+ let offset = 0;
1115
+ let currentLine = 1;
1116
+ for (let i = 0; i < text.length; i++) {
1117
+ if (currentLine === line) return offset;
1118
+ if (text[i] === "\n") {
1119
+ currentLine++;
1120
+ }
1121
+ offset++;
1122
+ }
1123
+ return offset;
1124
+ }
1125
+
1126
+ // src/plugins/optional/naming.ts
1127
+ function namingPlugin(_config = {}) {
1128
+ return {
1129
+ name: "naming-convention",
1130
+ version: "1.0.0",
1131
+ install(kernel) {
1132
+ kernel.registerRule({
1133
+ name: "naming-convention/file-naming",
1134
+ severity: "warning",
1135
+ description: "Enforces consistent file naming patterns based on directory",
1136
+ category: "quality",
1137
+ check(context) {
1138
+ const findings = [];
1139
+ const path = context.file.path;
1140
+ const fileName = path.split("/").pop() ?? "";
1141
+ if (path.includes("/services/") && !fileName.endsWith(".service.ts") && !fileName.endsWith(".test.ts")) {
1142
+ findings.push({
1143
+ message: `File in services/ should follow *.service.ts naming: "${fileName}"`,
1144
+ file: context.file.path,
1145
+ line: 1,
1146
+ column: 1,
1147
+ fix: { suggestion: "Rename file to follow *.service.ts convention." }
1148
+ });
1149
+ }
1150
+ if (path.includes("/controllers/") && !fileName.endsWith(".controller.ts") && !fileName.endsWith(".test.ts")) {
1151
+ findings.push({
1152
+ message: `File in controllers/ should follow *.controller.ts naming: "${fileName}"`,
1153
+ file: context.file.path,
1154
+ line: 1,
1155
+ column: 1,
1156
+ fix: { suggestion: "Rename file to follow *.controller.ts convention." }
1157
+ });
1158
+ }
1159
+ if (path.includes("/repositories/") && !fileName.endsWith(".repository.ts") && !fileName.endsWith(".test.ts")) {
1160
+ findings.push({
1161
+ message: `File in repositories/ should follow *.repository.ts naming: "${fileName}"`,
1162
+ file: context.file.path,
1163
+ line: 1,
1164
+ column: 1,
1165
+ fix: { suggestion: "Rename file to follow *.repository.ts convention." }
1166
+ });
1167
+ }
1168
+ return findings;
1169
+ }
1170
+ });
1171
+ }
1172
+ };
1173
+ }
1174
+
1175
+ // src/plugins/optional/api.ts
1176
+ function apiPlugin(_config = {}) {
1177
+ return {
1178
+ name: "api-consistency",
1179
+ version: "1.0.0",
1180
+ install(kernel) {
1181
+ kernel.registerRule({
1182
+ name: "api-consistency/endpoint-naming",
1183
+ severity: "info",
1184
+ description: "Checks REST API endpoint naming conventions",
1185
+ category: "quality",
1186
+ check(context) {
1187
+ if (context.file.role !== "controller") return [];
1188
+ const findings = [];
1189
+ context.walk(context.ast, {
1190
+ StringLiteral(node) {
1191
+ const str = node;
1192
+ const value = str.text;
1193
+ if (value.startsWith("/") && value.length > 1) {
1194
+ if (/\/[a-z]+[A-Z]/.test(value)) {
1195
+ const pos = context.ast.getLineAndCharacterOfPosition(str.getStart(context.ast));
1196
+ findings.push({
1197
+ message: `API endpoint "${value}" uses camelCase \u2014 prefer kebab-case`,
1198
+ file: context.file.path,
1199
+ line: pos.line + 1,
1200
+ column: pos.character + 1,
1201
+ fix: { suggestion: "Use kebab-case for URL paths (e.g., /user-profiles instead of /userProfiles)." }
1202
+ });
1203
+ }
1204
+ }
1205
+ }
1206
+ });
1207
+ return findings;
1208
+ }
1209
+ });
1210
+ }
1211
+ };
1212
+ }
1213
+
1214
+ // src/plugins/optional/test-guard.ts
1215
+ function testGuardPlugin(_config = {}) {
1216
+ return {
1217
+ name: "test-coverage-guard",
1218
+ version: "1.0.0",
1219
+ install(kernel) {
1220
+ kernel.registerRule({
1221
+ name: "test-coverage-guard/missing-tests",
1222
+ severity: "warning",
1223
+ description: "Ensures source files have corresponding test files",
1224
+ category: "quality",
1225
+ check(context) {
1226
+ if (context.file.role === "test" || context.file.role === "type" || context.file.role === "config") {
1227
+ return [];
1228
+ }
1229
+ const findings = [];
1230
+ const filePath = context.file.path;
1231
+ const testPaths = getExpectedTestPaths(filePath);
1232
+ const hasTest = testPaths.some((tp) => context.graph.files.has(tp));
1233
+ if (!hasTest) {
1234
+ findings.push({
1235
+ message: `No test file found for "${filePath}"`,
1236
+ file: context.file.path,
1237
+ line: 1,
1238
+ column: 1,
1239
+ fix: {
1240
+ /* v8 ignore next */
1241
+ suggestion: `Create a test file (e.g., ${testPaths[0] ?? filePath.replace(".ts", ".test.ts")}).`
1242
+ }
1243
+ });
1244
+ }
1245
+ return findings;
1246
+ }
1247
+ });
1248
+ }
1249
+ };
1250
+ }
1251
+ function getExpectedTestPaths(filePath) {
1252
+ const base = filePath.replace(/\.ts$/, "");
1253
+ return [
1254
+ `${base}.test.ts`,
1255
+ `${base}.spec.ts`,
1256
+ filePath.replace("src/", "tests/").replace(".ts", ".test.ts"),
1257
+ filePath.replace("src/", "tests/unit/").replace(".ts", ".test.ts")
1258
+ ];
1259
+ }
1260
+
1261
+ // src/plugins/optional/dep-audit.ts
1262
+ function depAuditPlugin(config = {}) {
1263
+ const fullConfig = {
1264
+ maxDepth: 5,
1265
+ ...config
1266
+ };
1267
+ return {
1268
+ name: "dependency-audit",
1269
+ version: "1.0.0",
1270
+ install(kernel) {
1271
+ kernel.registerRule({
1272
+ name: "dependency-audit/deep-imports",
1273
+ severity: "info",
1274
+ description: "Detects deeply nested import chains",
1275
+ category: "architecture",
1276
+ check(context) {
1277
+ const findings = [];
1278
+ const maxDepth = fullConfig.maxDepth ?? 5;
1279
+ const depth = calculateImportDepth(
1280
+ context.file.path,
1281
+ context.graph.dependencies.adjacency,
1282
+ /* @__PURE__ */ new Set()
1283
+ );
1284
+ if (depth > maxDepth) {
1285
+ findings.push({
1286
+ message: `File has import depth of ${depth} (max: ${maxDepth})`,
1287
+ file: context.file.path,
1288
+ line: 1,
1289
+ column: 1,
1290
+ fix: {
1291
+ suggestion: "Consider restructuring imports to reduce dependency depth."
1292
+ }
1293
+ });
1294
+ }
1295
+ return findings;
1296
+ }
1297
+ });
1298
+ }
1299
+ };
1300
+ }
1301
+ function calculateImportDepth(file, adjacency, visited) {
1302
+ if (visited.has(file)) return 0;
1303
+ visited.add(file);
1304
+ const deps = adjacency.get(file);
1305
+ if (!deps || deps.size === 0) return 0;
1306
+ let maxDepth = 0;
1307
+ for (const dep of deps) {
1308
+ const depth = calculateImportDepth(dep, adjacency, visited);
1309
+ maxDepth = Math.max(maxDepth, depth);
1310
+ }
1311
+ return maxDepth + 1;
1312
+ }
1313
+
1314
+ export { apiPlugin, architecturePlugin, depAuditPlugin, namingPlugin, performancePlugin, qualityPlugin, securityPlugin, testGuardPlugin };
1315
+ //# sourceMappingURL=index.js.map
1316
+ //# sourceMappingURL=index.js.map