@girardelli/architect-core 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/dist/src/core/analyzer.d.ts +42 -0
  2. package/dist/src/core/analyzer.js +431 -0
  3. package/dist/src/core/analyzer.js.map +1 -0
  4. package/dist/src/core/analyzers/forecast.d.ts +84 -0
  5. package/dist/src/core/analyzers/forecast.js +338 -0
  6. package/dist/src/core/analyzers/forecast.js.map +1 -0
  7. package/dist/src/core/analyzers/index.d.ts +9 -0
  8. package/dist/src/core/analyzers/index.js +7 -0
  9. package/dist/src/core/analyzers/index.js.map +1 -0
  10. package/dist/src/core/analyzers/temporal-scorer.d.ts +71 -0
  11. package/dist/src/core/analyzers/temporal-scorer.js +141 -0
  12. package/dist/src/core/analyzers/temporal-scorer.js.map +1 -0
  13. package/dist/src/core/anti-patterns.d.ts +28 -0
  14. package/dist/src/core/anti-patterns.js +264 -0
  15. package/dist/src/core/anti-patterns.js.map +1 -0
  16. package/dist/src/core/ast/ast-parser.interface.d.ts +20 -0
  17. package/dist/src/core/ast/ast-parser.interface.js +2 -0
  18. package/dist/src/core/ast/ast-parser.interface.js.map +1 -0
  19. package/dist/src/core/ast/path-resolver.d.ts +13 -0
  20. package/dist/src/core/ast/path-resolver.js +54 -0
  21. package/dist/src/core/ast/path-resolver.js.map +1 -0
  22. package/dist/src/core/ast/tree-sitter-parser.d.ts +10 -0
  23. package/dist/src/core/ast/tree-sitter-parser.js +142 -0
  24. package/dist/src/core/ast/tree-sitter-parser.js.map +1 -0
  25. package/dist/src/core/config.d.ts +11 -0
  26. package/dist/src/core/config.js +112 -0
  27. package/dist/src/core/config.js.map +1 -0
  28. package/dist/src/core/diagram.d.ts +9 -0
  29. package/dist/src/core/diagram.js +101 -0
  30. package/dist/src/core/diagram.js.map +1 -0
  31. package/dist/src/core/i18n.d.ts +14 -0
  32. package/dist/src/core/i18n.js +54 -0
  33. package/dist/src/core/i18n.js.map +1 -0
  34. package/dist/src/core/locales/en.d.ts +2 -0
  35. package/dist/src/core/locales/en.js +337 -0
  36. package/dist/src/core/locales/en.js.map +1 -0
  37. package/dist/src/core/locales/pt-BR.d.ts +172 -0
  38. package/dist/src/core/locales/pt-BR.js +337 -0
  39. package/dist/src/core/locales/pt-BR.js.map +1 -0
  40. package/dist/src/core/locales/types.d.ts +86 -0
  41. package/dist/src/core/locales/types.js +2 -0
  42. package/dist/src/core/locales/types.js.map +1 -0
  43. package/dist/src/core/plugin-loader.d.ts +11 -0
  44. package/dist/src/core/plugin-loader.js +67 -0
  45. package/dist/src/core/plugin-loader.js.map +1 -0
  46. package/dist/src/core/project-summarizer.d.ts +16 -0
  47. package/dist/src/core/project-summarizer.js +37 -0
  48. package/dist/src/core/project-summarizer.js.map +1 -0
  49. package/dist/src/core/refactor-engine.d.ts +18 -0
  50. package/dist/src/core/refactor-engine.js +87 -0
  51. package/dist/src/core/refactor-engine.js.map +1 -0
  52. package/dist/src/core/rules/barrel-optimizer.d.ts +13 -0
  53. package/dist/src/core/rules/barrel-optimizer.js +76 -0
  54. package/dist/src/core/rules/barrel-optimizer.js.map +1 -0
  55. package/dist/src/core/rules/dead-code-detector.d.ts +21 -0
  56. package/dist/src/core/rules/dead-code-detector.js +116 -0
  57. package/dist/src/core/rules/dead-code-detector.js.map +1 -0
  58. package/dist/src/core/rules/hub-splitter.d.ts +13 -0
  59. package/dist/src/core/rules/hub-splitter.js +117 -0
  60. package/dist/src/core/rules/hub-splitter.js.map +1 -0
  61. package/dist/src/core/rules/import-organizer.d.ts +13 -0
  62. package/dist/src/core/rules/import-organizer.js +84 -0
  63. package/dist/src/core/rules/import-organizer.js.map +1 -0
  64. package/dist/src/core/rules/module-grouper.d.ts +13 -0
  65. package/dist/src/core/rules/module-grouper.js +116 -0
  66. package/dist/src/core/rules/module-grouper.js.map +1 -0
  67. package/dist/src/core/rules-engine.d.ts +7 -0
  68. package/dist/src/core/rules-engine.js +89 -0
  69. package/dist/src/core/rules-engine.js.map +1 -0
  70. package/dist/src/core/scorer.d.ts +15 -0
  71. package/dist/src/core/scorer.js +165 -0
  72. package/dist/src/core/scorer.js.map +1 -0
  73. package/dist/src/core/summarizer/keyword-extractor.d.ts +6 -0
  74. package/dist/src/core/summarizer/keyword-extractor.js +38 -0
  75. package/dist/src/core/summarizer/keyword-extractor.js.map +1 -0
  76. package/dist/src/core/summarizer/module-inferrer.d.ts +11 -0
  77. package/dist/src/core/summarizer/module-inferrer.js +171 -0
  78. package/dist/src/core/summarizer/module-inferrer.js.map +1 -0
  79. package/dist/src/core/summarizer/package-reader.d.ts +3 -0
  80. package/dist/src/core/summarizer/package-reader.js +33 -0
  81. package/dist/src/core/summarizer/package-reader.js.map +1 -0
  82. package/dist/src/core/summarizer/purpose-inferrer.d.ts +8 -0
  83. package/dist/src/core/summarizer/purpose-inferrer.js +179 -0
  84. package/dist/src/core/summarizer/purpose-inferrer.js.map +1 -0
  85. package/dist/src/core/summarizer/readme-reader.d.ts +3 -0
  86. package/dist/src/core/summarizer/readme-reader.js +24 -0
  87. package/dist/src/core/summarizer/readme-reader.js.map +1 -0
  88. package/dist/src/core/types/architect-rules.d.ts +27 -0
  89. package/dist/src/core/types/architect-rules.js +2 -0
  90. package/dist/src/core/types/architect-rules.js.map +1 -0
  91. package/dist/src/core/types/core.d.ts +87 -0
  92. package/dist/src/core/types/core.js +2 -0
  93. package/dist/src/core/types/core.js.map +1 -0
  94. package/dist/src/core/types/infrastructure.d.ts +38 -0
  95. package/dist/src/core/types/infrastructure.js +2 -0
  96. package/dist/src/core/types/infrastructure.js.map +1 -0
  97. package/dist/src/core/types/plugin.d.ts +12 -0
  98. package/dist/src/core/types/plugin.js +2 -0
  99. package/dist/src/core/types/plugin.js.map +1 -0
  100. package/dist/src/core/types/rules.d.ts +53 -0
  101. package/dist/src/core/types/rules.js +2 -0
  102. package/dist/src/core/types/rules.js.map +1 -0
  103. package/dist/src/core/types/summarizer.d.ts +12 -0
  104. package/dist/src/core/types/summarizer.js +2 -0
  105. package/dist/src/core/types/summarizer.js.map +1 -0
  106. package/dist/src/infrastructure/git-cache.d.ts +6 -0
  107. package/dist/src/infrastructure/git-cache.js +41 -0
  108. package/dist/src/infrastructure/git-cache.js.map +1 -0
  109. package/dist/src/infrastructure/git-history.d.ts +112 -0
  110. package/dist/src/infrastructure/git-history.js +340 -0
  111. package/dist/src/infrastructure/git-history.js.map +1 -0
  112. package/dist/src/infrastructure/logger.d.ts +20 -0
  113. package/dist/src/infrastructure/logger.js +57 -0
  114. package/dist/src/infrastructure/logger.js.map +1 -0
  115. package/dist/src/infrastructure/scanner.d.ts +31 -0
  116. package/dist/src/infrastructure/scanner.js +334 -0
  117. package/dist/src/infrastructure/scanner.js.map +1 -0
  118. package/dist/tests/analyzers-integration.test.d.ts +7 -0
  119. package/dist/tests/analyzers-integration.test.js +140 -0
  120. package/dist/tests/analyzers-integration.test.js.map +1 -0
  121. package/dist/tests/anti-patterns.test.d.ts +1 -0
  122. package/dist/tests/anti-patterns.test.js +81 -0
  123. package/dist/tests/anti-patterns.test.js.map +1 -0
  124. package/dist/tests/ast-parser.test.d.ts +1 -0
  125. package/dist/tests/ast-parser.test.js +94 -0
  126. package/dist/tests/ast-parser.test.js.map +1 -0
  127. package/dist/tests/fixtures/monorepo/packages/app/src/index.d.ts +1 -0
  128. package/dist/tests/fixtures/monorepo/packages/app/src/index.js +9 -0
  129. package/dist/tests/fixtures/monorepo/packages/app/src/index.js.map +1 -0
  130. package/dist/tests/fixtures/monorepo/packages/core/src/index.d.ts +2 -0
  131. package/dist/tests/fixtures/monorepo/packages/core/src/index.js +11 -0
  132. package/dist/tests/fixtures/monorepo/packages/core/src/index.js.map +1 -0
  133. package/dist/tests/forecast.test.d.ts +7 -0
  134. package/dist/tests/forecast.test.js +380 -0
  135. package/dist/tests/forecast.test.js.map +1 -0
  136. package/dist/tests/git-history.test.d.ts +7 -0
  137. package/dist/tests/git-history.test.js +193 -0
  138. package/dist/tests/git-history.test.js.map +1 -0
  139. package/dist/tests/i18n.test.d.ts +1 -0
  140. package/dist/tests/i18n.test.js +39 -0
  141. package/dist/tests/i18n.test.js.map +1 -0
  142. package/dist/tests/monorepo-scan.test.d.ts +11 -0
  143. package/dist/tests/monorepo-scan.test.js +143 -0
  144. package/dist/tests/monorepo-scan.test.js.map +1 -0
  145. package/dist/tests/plugin-loader.test.d.ts +1 -0
  146. package/dist/tests/plugin-loader.test.js +31 -0
  147. package/dist/tests/plugin-loader.test.js.map +1 -0
  148. package/dist/tests/rules-engine.test.d.ts +1 -0
  149. package/dist/tests/rules-engine.test.js +112 -0
  150. package/dist/tests/rules-engine.test.js.map +1 -0
  151. package/dist/tests/scanner.test.d.ts +1 -0
  152. package/dist/tests/scanner.test.js +44 -0
  153. package/dist/tests/scanner.test.js.map +1 -0
  154. package/dist/tests/scorer.test.d.ts +1 -0
  155. package/dist/tests/scorer.test.js +610 -0
  156. package/dist/tests/scorer.test.js.map +1 -0
  157. package/dist/tests/temporal-scorer.test.d.ts +7 -0
  158. package/dist/tests/temporal-scorer.test.js +239 -0
  159. package/dist/tests/temporal-scorer.test.js.map +1 -0
  160. package/package.json +29 -0
  161. package/src/core/analyzer.ts +499 -0
  162. package/src/core/analyzers/forecast.ts +497 -0
  163. package/src/core/analyzers/index.ts +33 -0
  164. package/src/core/analyzers/temporal-scorer.ts +227 -0
  165. package/src/core/anti-patterns.ts +324 -0
  166. package/src/core/ast/ast-parser.interface.ts +21 -0
  167. package/src/core/ast/path-resolver.ts +61 -0
  168. package/src/core/ast/tree-sitter-parser.ts +158 -0
  169. package/src/core/config.ts +125 -0
  170. package/src/core/diagram.ts +129 -0
  171. package/src/core/i18n.ts +64 -0
  172. package/src/core/locales/en.ts +340 -0
  173. package/src/core/locales/pt-BR.ts +341 -0
  174. package/src/core/locales/types.ts +95 -0
  175. package/src/core/plugin-loader.ts +80 -0
  176. package/src/core/project-summarizer.ts +42 -0
  177. package/src/core/refactor-engine.ts +112 -0
  178. package/src/core/rules/barrel-optimizer.ts +99 -0
  179. package/src/core/rules/dead-code-detector.ts +134 -0
  180. package/src/core/rules/hub-splitter.ts +135 -0
  181. package/src/core/rules/import-organizer.ts +100 -0
  182. package/src/core/rules/module-grouper.ts +133 -0
  183. package/src/core/rules-engine.ts +100 -0
  184. package/src/core/scorer.ts +181 -0
  185. package/src/core/summarizer/keyword-extractor.ts +53 -0
  186. package/src/core/summarizer/module-inferrer.ts +194 -0
  187. package/src/core/summarizer/package-reader.ts +34 -0
  188. package/src/core/summarizer/purpose-inferrer.ts +197 -0
  189. package/src/core/summarizer/readme-reader.ts +24 -0
  190. package/src/core/types/architect-rules.ts +29 -0
  191. package/src/core/types/core.ts +94 -0
  192. package/src/core/types/infrastructure.ts +41 -0
  193. package/src/core/types/plugin.ts +19 -0
  194. package/src/core/types/rules.ts +51 -0
  195. package/src/core/types/summarizer.ts +8 -0
  196. package/src/infrastructure/git-cache.ts +52 -0
  197. package/src/infrastructure/git-history.ts +496 -0
  198. package/src/infrastructure/logger.ts +68 -0
  199. package/src/infrastructure/scanner.ts +349 -0
  200. package/tests/analyzers-integration.test.ts +174 -0
  201. package/tests/anti-patterns.test.ts +95 -0
  202. package/tests/ast-parser.test.ts +102 -0
  203. package/tests/fixtures/monorepo/package.json +6 -0
  204. package/tests/fixtures/monorepo/packages/app/package.json +12 -0
  205. package/tests/fixtures/monorepo/packages/app/src/index.ts +6 -0
  206. package/tests/fixtures/monorepo/packages/core/package.json +7 -0
  207. package/tests/fixtures/monorepo/packages/core/src/index.ts +7 -0
  208. package/tests/forecast.test.ts +504 -0
  209. package/tests/git-history.test.ts +254 -0
  210. package/tests/i18n.test.ts +47 -0
  211. package/tests/monorepo-scan.test.ts +170 -0
  212. package/tests/plugin-loader.test.ts +40 -0
  213. package/tests/rules-engine.test.ts +131 -0
  214. package/tests/scanner.test.ts +54 -0
  215. package/tests/scorer.test.ts +675 -0
  216. package/tests/temporal-scorer.test.ts +306 -0
  217. package/tsconfig.json +9 -0
@@ -0,0 +1,135 @@
1
+ import { basename, dirname} from 'path';
2
+ import { AnalysisReport } from '../types/core.js';
3
+ import { RefactorRule, RefactorStep, FileOperation } from '../types/rules.js';
4
+
5
+ /**
6
+ * Hub Splitter Rule (Tier 1)
7
+ * Detects files with many connections and generates split plans.
8
+ * A "hub" is a file that many other files depend on, creating tight coupling.
9
+ */
10
+ export class HubSplitterRule implements RefactorRule {
11
+ name = 'hub-splitter';
12
+ tier = 1 as const;
13
+
14
+ analyze(report: AnalysisReport, _projectPath: string): RefactorStep[] {
15
+ const steps: RefactorStep[] = [];
16
+
17
+ // Count connections per node
18
+ const connectionCount: Record<string, { incoming: string[]; outgoing: string[] }> = {};
19
+
20
+ for (const edge of report.dependencyGraph.edges) {
21
+ if (!connectionCount[edge.from]) connectionCount[edge.from] = { incoming: [], outgoing: [] };
22
+ if (!connectionCount[edge.to]) connectionCount[edge.to] = { incoming: [], outgoing: [] };
23
+ connectionCount[edge.from].outgoing.push(edge.to);
24
+ connectionCount[edge.to].incoming.push(edge.from);
25
+ }
26
+
27
+ // Find hubs (5+ incoming connections, not barrel files)
28
+ const barrelFiles = new Set(['__init__.py', 'index.ts', 'index.js', 'index.tsx', 'mod.rs']);
29
+
30
+ for (const [file, connections] of Object.entries(connectionCount)) {
31
+ const fileName = basename(file);
32
+ if (barrelFiles.has(fileName)) continue;
33
+
34
+ // Ignora Tipos base/DTOs - Alto acoplamento em tipos é sinal de maturidade, não gargalo
35
+ const lowerFile = file.toLowerCase();
36
+ if (lowerFile.includes('types') || lowerFile.includes('interface') || lowerFile.includes('/types/')) {
37
+ continue;
38
+ }
39
+
40
+ // Tolerância Arquitetural: Eleva de 5 pra 8 dependentes pra engatilhar quebra (Módulos Coesos maduros)
41
+ if (connections.incoming.length < 8) continue;
42
+
43
+ const operations: FileOperation[] = [];
44
+
45
+ // Determine if this is a dot-notation module or a real file
46
+ const isDotNotation = !file.includes('/') && !file.includes('\\');
47
+ const moduleName = isDotNotation
48
+ ? file.split('.').pop() || file
49
+ : fileName.replace(/\.[^.]+$/, '');
50
+ const moduleDir = isDotNotation
51
+ ? file.split('.').slice(0, -1).join('/')
52
+ : dirname(file);
53
+ const ext = isDotNotation ? 'py' : (fileName.split('.').pop() || 'py');
54
+
55
+ // Analyze what dependents import to suggest groupings
56
+ const dependentGroups = this.groupDependents(connections.incoming);
57
+
58
+ // Suggest splitting into domain modules
59
+ if (dependentGroups.length >= 2) {
60
+ for (const group of dependentGroups) {
61
+ const newFileName = `${moduleName}_${group.name}.${ext}`;
62
+ const newPath = moduleDir ? `${moduleDir}/${newFileName}` : newFileName;
63
+
64
+ operations.push({
65
+ type: 'CREATE',
66
+ path: newPath,
67
+ description: `Create \`${newFileName}\` with functionality used by: ${group.dependents.join(', ')}`,
68
+ content: ext === 'py'
69
+ ? `"""${moduleName}_${group.name} — extracted from ${moduleName}."""\n# Used by: ${group.dependents.join(', ')}\n`
70
+ : `// ${moduleName}_${group.name} — extracted from ${moduleName}\n// Used by: ${group.dependents.join(', ')}\n`,
71
+ });
72
+ }
73
+
74
+ // Update imports in all dependents
75
+ for (const dependent of connections.incoming) {
76
+ operations.push({
77
+ type: 'MODIFY',
78
+ path: dependent,
79
+ description: `Update imports in \`${basename(dependent)}\` to use new split modules`,
80
+ });
81
+ }
82
+
83
+ // Mark original for refactoring
84
+ operations.push({
85
+ type: 'MODIFY',
86
+ path: isDotNotation ? `${moduleDir}/${moduleName}.${ext}` : file,
87
+ description: `Refactor \`${moduleName}.${ext}\` — extract grouped functionality to new modules`,
88
+ });
89
+ }
90
+
91
+ if (operations.length > 0) {
92
+ steps.push({
93
+ id: 0,
94
+ tier: 1,
95
+ rule: this.name,
96
+ priority: connections.incoming.length >= 8 ? 'CRITICAL' : 'HIGH',
97
+ title: `Split hub file: ${moduleName}.${ext}`,
98
+ description: `\`${file}\` has ${connections.incoming.length} incoming connections. ` +
99
+ `Split into ${dependentGroups.length} focused modules to reduce coupling.`,
100
+ rationale: `High fan-in (${connections.incoming.length} files depend on this) creates a bottleneck. ` +
101
+ `Changes to this file ripple to ${connections.incoming.length} other files. ` +
102
+ `Splitting by usage pattern reduces blast radius.`,
103
+ operations,
104
+ scoreImpact: [
105
+ { metric: 'coupling', before: report.score.breakdown.coupling, after: Math.min(95, report.score.breakdown.coupling + 15) },
106
+ ],
107
+ aiPrompt: `Analyze the file \`${file}\`. Based on its incoming connections, it acts as a coupling bottleneck. Please split this file into the following smaller modules:\n` +
108
+ dependentGroups.map(g => `- \`${moduleName}_${g.name}.${ext}\`: Extract functionality specific to these dependents: ${g.dependents.join(', ')}`).join('\n') +
109
+ `\n\nAfter splitting, securely and automatically update all ${connections.incoming.length} dependent files to import from the new specific modules instead of the monolithic \`${moduleName}.${ext}\`. DO NOT remove any functionality, only move it.`,
110
+ });
111
+ }
112
+ }
113
+
114
+ return steps;
115
+ }
116
+
117
+ private groupDependents(
118
+ dependents: string[]
119
+ ): Array<{ name: string; dependents: string[] }> {
120
+ // Group by top-level directory
121
+ const groups: Record<string, string[]> = {};
122
+
123
+ for (const dep of dependents) {
124
+ const parts = dep.includes('/') ? dep.split('/') : dep.split('.');
125
+ const groupName = parts.length >= 2 ? parts[parts.length - 2] : 'core';
126
+ if (!groups[groupName]) groups[groupName] = [];
127
+ groups[groupName].push(basename(dep));
128
+ }
129
+
130
+ return Object.entries(groups).map(([name, deps]) => ({
131
+ name,
132
+ dependents: deps,
133
+ }));
134
+ }
135
+ }
@@ -0,0 +1,100 @@
1
+ import { basename, dirname } from 'path';
2
+ import { AnalysisReport } from '../types/core.js';
3
+ import { RefactorRule, RefactorStep, FileOperation } from '../types/rules.js';
4
+
5
+ /**
6
+ * Import Organizer Rule (Tier 1)
7
+ * Detects files that import from too many different modules (cross-boundary).
8
+ * Suggests dependency injection or facade patterns.
9
+ */
10
+ export class ImportOrganizerRule implements RefactorRule {
11
+ name = 'import-organizer';
12
+ tier = 1 as const;
13
+
14
+ analyze(report: AnalysisReport, _projectPath: string): RefactorStep[] {
15
+ const steps: RefactorStep[] = [];
16
+
17
+ // Find files that import from many different directories
18
+ const crossBoundary: Record<string, { targets: Set<string>; dirs: Set<string> }> = {};
19
+
20
+ for (const edge of report.dependencyGraph.edges) {
21
+ const fromDir = dirname(edge.from);
22
+ const toDir = dirname(edge.to);
23
+
24
+ if (!crossBoundary[edge.from]) {
25
+ crossBoundary[edge.from] = { targets: new Set(), dirs: new Set() };
26
+ }
27
+ crossBoundary[edge.from].targets.add(edge.to);
28
+
29
+ if (fromDir !== toDir) {
30
+ crossBoundary[edge.from].dirs.add(toDir);
31
+ }
32
+ }
33
+
34
+ // Outliers reais espalhados consumindo +5 diretórios. Testa bypass em unit-tests (.test.) naturais de altíssima injeção (mock).
35
+ const violators = Object.entries(crossBoundary)
36
+ .filter(([fileName, data]) => data.dirs.size >= 5 && !fileName.includes('.test.'))
37
+ .sort((a, b) => b[1].dirs.size - a[1].dirs.size);
38
+
39
+ for (const [file, data] of violators) {
40
+ const operations: FileOperation[] = [];
41
+ const fileName = basename(file);
42
+ const fileDir = dirname(file);
43
+
44
+ // Suggest creating a facade/service layer
45
+ const ext = fileName.split('.').pop() || 'py';
46
+ const nameBase = fileName.replace(/\.[^.]+$/, '');
47
+ const facadePath = `${fileDir}/${nameBase}_deps.${ext}`;
48
+
49
+ operations.push({
50
+ type: 'CREATE',
51
+ path: facadePath,
52
+ description: `Create dependency facade \`${basename(facadePath)}\` — centralizes ${data.dirs.size} cross-module imports`,
53
+ content: this.generateFacadeContent(ext, Array.from(data.targets), Array.from(data.dirs)),
54
+ });
55
+
56
+ operations.push({
57
+ type: 'MODIFY',
58
+ path: file,
59
+ description: `Refactor \`${fileName}\` to import from local facade instead of ${data.dirs.size} different modules`,
60
+ });
61
+
62
+ steps.push({
63
+ id: 0,
64
+ tier: 1,
65
+ rule: this.name,
66
+ priority: data.dirs.size >= 5 ? 'HIGH' : 'MEDIUM',
67
+ title: `Reduce cross-boundary imports: ${fileName}`,
68
+ description: `\`${file}\` imports from ${data.dirs.size} different modules: ` +
69
+ `${Array.from(data.dirs).map((d) => `\`${d}\``).join(', ')}. ` +
70
+ `Consider using a facade or dependency injection.`,
71
+ rationale: `Files with imports scattered across many modules have high afferent coupling. ` +
72
+ `A facade centralizes these dependencies, making the file easier to test (mock one facade) ` +
73
+ `and reducing the impact of changes in dependent modules.`,
74
+ operations,
75
+ scoreImpact: [
76
+ { metric: 'cohesion', before: report.score.breakdown.cohesion, after: Math.min(95, report.score.breakdown.cohesion + 5) },
77
+ { metric: 'coupling', before: report.score.breakdown.coupling, after: Math.min(95, report.score.breakdown.coupling + 5) },
78
+ ],
79
+ aiPrompt: `Analyze the file \`${file}\` which currently imports from ${data.dirs.size} cross-boundary distinct directories/modules (${Array.from(data.dirs).join(', ')}).\nPlease refactor this to use the Dependency Injection (DI) or Facade pattern:\n1. Extract these imports into a dedicated file \`${facadePath}\`.\n2. Re-export or bundle them appropriately.\n3. Update \`${file}\` to import strictly from the new \`${facadePath}\` rather than directly reaching into multiple disjointed modules, lowering its afferent coupling.`,
80
+ });
81
+ }
82
+
83
+ return steps;
84
+ }
85
+
86
+ private generateFacadeContent(ext: string, targets: string[], dirs: string[]): string {
87
+ if (ext === 'py') {
88
+ const imports = targets
89
+ .map((t) => `# from ${t.replace(/\//g, '.')} import ...`)
90
+ .join('\n');
91
+ return `"""Dependency facade — centralizes cross-module imports."""\n\n${imports}\n\n# Re-export what ${dirs.length} modules need\n`;
92
+ }
93
+
94
+ // JS/TS
95
+ const imports = targets
96
+ .map((t) => `// export { ... } from '${t}';`)
97
+ .join('\n');
98
+ return `/**\n * Dependency facade — centralizes cross-module imports.\n */\n\n${imports}\n`;
99
+ }
100
+ }
@@ -0,0 +1,133 @@
1
+ import { basename, dirname, join } from 'path';
2
+ import { AnalysisReport } from '../types/core.js';
3
+ import { RefactorRule, RefactorStep, FileOperation } from '../types/rules.js';
4
+
5
+ /**
6
+ * Module Grouper Rule (Tier 1)
7
+ * Analyzes which files are frequently imported together and suggests
8
+ * grouping them into cohesive modules/packages.
9
+ */
10
+ export class ModuleGrouperRule implements RefactorRule {
11
+ name = 'module-grouper';
12
+ tier = 1 as const;
13
+
14
+ analyze(report: AnalysisReport, _projectPath: string): RefactorStep[] {
15
+ const steps: RefactorStep[] = [];
16
+
17
+ // Build co-import matrix: which files are imported together?
18
+ const coImportCount: Record<string, Record<string, number>> = {};
19
+
20
+ // For each source file, see what it imports
21
+ const importsBySource: Record<string, string[]> = {};
22
+ for (const edge of report.dependencyGraph.edges) {
23
+ if (!importsBySource[edge.from]) importsBySource[edge.from] = [];
24
+ importsBySource[edge.from].push(edge.to);
25
+ }
26
+
27
+ // Count co-imports
28
+ for (const [_source, targets] of Object.entries(importsBySource)) {
29
+ for (let i = 0; i < targets.length; i++) {
30
+ for (let j = i + 1; j < targets.length; j++) {
31
+ const a = targets[i];
32
+ const b = targets[j];
33
+ if (!coImportCount[a]) coImportCount[a] = {};
34
+ if (!coImportCount[b]) coImportCount[b] = {};
35
+ coImportCount[a][b] = (coImportCount[a][b] || 0) + 1;
36
+ coImportCount[b][a] = (coImportCount[b][a] || 0) + 1;
37
+ }
38
+ }
39
+ }
40
+
41
+ // Find clusters: files that are always imported together
42
+ const clusters: Array<{ files: string[]; coImportScore: number }> = [];
43
+ const visited = new Set<string>();
44
+
45
+ for (const [fileA, partners] of Object.entries(coImportCount)) {
46
+ if (visited.has(fileA)) continue;
47
+
48
+ const strongPartners = Object.entries(partners)
49
+ .filter(([_, count]) => count >= 3)
50
+ .sort((a, b) => b[1] - a[1]);
51
+
52
+ if (strongPartners.length >= 2) {
53
+ const cluster = [fileA, ...strongPartners.map(([f]) => f)];
54
+ const inSameDir = cluster.every(
55
+ (f) => dirname(f) === dirname(cluster[0])
56
+ );
57
+
58
+ // Only suggest if NOT already in the same directory
59
+ if (!inSameDir) {
60
+ // Arquitetura Limpa: Impede arrastar Módulos Transversais (DTOs/Types) para se tornarem fechados em Pastas de Domínios
61
+ const hasTypes = cluster.some(f => {
62
+ const lowerF = f.toLowerCase();
63
+ return lowerF.includes('types') || lowerF.includes('interface') || lowerF.includes('/types/');
64
+ });
65
+ if (hasTypes) continue;
66
+
67
+ const score = strongPartners.reduce((sum, [_, c]) => sum + c, 0);
68
+ clusters.push({ files: cluster, coImportScore: score });
69
+ cluster.forEach((f) => visited.add(f));
70
+ }
71
+ }
72
+ }
73
+
74
+ // Generate steps for each cluster
75
+ for (const cluster of clusters.slice(0, 3)) {
76
+ const operations: FileOperation[] = [];
77
+ const clusterName = this.suggestModuleName(cluster.files);
78
+ const targetDir = `${dirname(cluster.files[0])}/${clusterName}`;
79
+
80
+ // Create new module directory
81
+ operations.push({
82
+ type: 'CREATE',
83
+ path: `${targetDir}/__init__.py`,
84
+ description: `Create new module \`${clusterName}/\` to group ${cluster.files.length} co-dependent files`,
85
+ content: `"""Module ${clusterName} — grouped by co-import pattern."""\n`,
86
+ });
87
+
88
+ // Move files
89
+ for (const file of cluster.files) {
90
+ const newPath = join(targetDir, basename(file));
91
+ operations.push({
92
+ type: 'MOVE',
93
+ path: file,
94
+ newPath,
95
+ description: `Move \`${basename(file)}\` → \`${clusterName}/${basename(file)}\``,
96
+ });
97
+ }
98
+
99
+ steps.push({
100
+ id: 0,
101
+ tier: 1,
102
+ rule: this.name,
103
+ priority: 'MEDIUM',
104
+ title: `Group co-dependent files into \`${clusterName}/\``,
105
+ description: `Files ${cluster.files.map((f) => `\`${basename(f)}\``).join(', ')} ` +
106
+ `are frequently imported together (co-import score: ${cluster.coImportScore}). ` +
107
+ `Grouping them improves cohesion.`,
108
+ rationale: `Files that are frequently imported together belong in the same module. ` +
109
+ `This improves discoverability and reduces the cognitive load of understanding ` +
110
+ `which files work together.`,
111
+ operations,
112
+ scoreImpact: [
113
+ { metric: 'cohesion', before: report.score.breakdown.cohesion, after: Math.min(95, report.score.breakdown.cohesion + 10) },
114
+ { metric: 'modularity', before: report.score.breakdown.modularity, after: Math.min(95, report.score.breakdown.modularity + 5) },
115
+ ],
116
+ aiPrompt: `Analyze the files: ${cluster.files.map(f => `\`${f}\``).join(', ')}.\nThese files are frequently imported together but are currently scattered across different directories.\nPlease refactor to improve cohesion:\n1. Create a new directory named \`${targetDir}\`.\n2. Move these files solidly into this new directory.\n3. Add an index/barrel file if appropriate.\n4. Securely scan the entire project to fix all broken imports resulting from this move.`,
117
+ });
118
+ }
119
+
120
+ return steps;
121
+ }
122
+
123
+ private suggestModuleName(files: string[]): string {
124
+ // Try to infer a common theme from filenames
125
+ const names = files.map((f) => basename(f).replace(/\.[^.]+$/, '').toLowerCase());
126
+ const commonParts = names[0].split(/[_-]/).filter((part) =>
127
+ names.every((n) => n.includes(part))
128
+ );
129
+
130
+ if (commonParts.length > 0) return commonParts[0];
131
+ return 'shared';
132
+ }
133
+ }
@@ -0,0 +1,100 @@
1
+ import { AnalysisReport } from './types/core.js';
2
+ import { ArchitectRules, RuleViolation, ValidationResult } from './types/architect-rules.js';
3
+
4
+ export class RulesEngine {
5
+ public validate(report: AnalysisReport, rules: ArchitectRules): ValidationResult {
6
+ const violations: RuleViolation[] = [];
7
+
8
+ this.checkQualityGates(report, rules, violations);
9
+ this.checkBoundaries(report, rules, violations);
10
+
11
+ return {
12
+ success: violations.every(v => v.level !== 'error'),
13
+ violations,
14
+ };
15
+ }
16
+
17
+ private checkQualityGates(report: AnalysisReport, rules: ArchitectRules, violations: RuleViolation[]) {
18
+ if (!rules.quality_gates) return;
19
+
20
+ const { min_overall_score, max_critical_anti_patterns, max_high_anti_patterns } = rules.quality_gates;
21
+
22
+ // 1. Minimum Overall Score (Error)
23
+ if (min_overall_score !== undefined && report.score.overall < min_overall_score) {
24
+ violations.push({
25
+ level: 'error',
26
+ rule: 'quality_gates.min_overall_score',
27
+ message: `Overall score (${report.score.overall}) is below the minimum required (${min_overall_score}).`,
28
+ actual: report.score.overall,
29
+ expected: min_overall_score,
30
+ });
31
+ }
32
+
33
+ // 2. Max Critical Anti-Patterns (Error)
34
+ if (max_critical_anti_patterns !== undefined) {
35
+ const criticalCount = report.antiPatterns.filter(p => p.severity === 'CRITICAL').length;
36
+ if (criticalCount > max_critical_anti_patterns) {
37
+ violations.push({
38
+ level: 'error',
39
+ rule: 'quality_gates.max_critical_anti_patterns',
40
+ message: `Too many CRITICAL anti-patterns detected (${criticalCount}). Maximum allowed is ${max_critical_anti_patterns}.`,
41
+ actual: criticalCount,
42
+ expected: max_critical_anti_patterns,
43
+ });
44
+ }
45
+ }
46
+
47
+ // 3. Max High Anti-Patterns (Warning)
48
+ if (max_high_anti_patterns !== undefined) {
49
+ const highCount = report.antiPatterns.filter(p => p.severity === 'HIGH').length;
50
+ if (highCount > max_high_anti_patterns) {
51
+ violations.push({
52
+ level: 'warning',
53
+ rule: 'quality_gates.max_high_anti_patterns',
54
+ message: `High number of HIGH severity anti-patterns (${highCount}). Maximum recommended is ${max_high_anti_patterns}.`,
55
+ actual: highCount,
56
+ expected: max_high_anti_patterns,
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ private checkBoundaries(report: AnalysisReport, rules: ArchitectRules, violations: RuleViolation[]) {
63
+ if (!rules.boundaries) return;
64
+
65
+ const { allow_circular_dependencies, banned_imports } = rules.boundaries;
66
+
67
+ // 1. Allow Circular Dependencies
68
+ if (allow_circular_dependencies === false) {
69
+ const hasCycles = report.antiPatterns.filter(p => p.name === 'Circular Dependency');
70
+ if (hasCycles.length > 0) {
71
+ violations.push({
72
+ level: 'error',
73
+ rule: 'boundaries.allow_circular_dependencies',
74
+ message: `Circular dependencies are strictly forbidden, but ${hasCycles.length} were found.`,
75
+ actual: hasCycles.length,
76
+ expected: 0,
77
+ });
78
+ }
79
+ }
80
+
81
+ // 2. Banned Imports (Checks the AST Dependency Graph edges)
82
+ if (banned_imports && banned_imports.length > 0) {
83
+ for (const edge of report.dependencyGraph.edges) {
84
+ // Look through 'to' targets
85
+ for (const banned of banned_imports) {
86
+ if (edge.to.includes(banned)) {
87
+ // Found a banned import!
88
+ violations.push({
89
+ level: 'error',
90
+ rule: 'boundaries.banned_imports',
91
+ message: `Banned import detected: File '${edge.from}' imports '${banned}'.`,
92
+ actual: edge.to,
93
+ expected: `Not to include ${banned}`,
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,181 @@
1
+ import { ArchitectureScore, DependencyEdge, AntiPattern} from './types/core.js';
2
+ import { basename } from 'path';
3
+
4
+ export class ArchitectureScorer {
5
+ /**
6
+ * Barrel/index files that naturally have many connections and should be
7
+ * excluded from coupling max-edge penalty calculations.
8
+ */
9
+ private static readonly BARREL_FILES = new Set([
10
+ '__init__.py', 'index.ts', 'index.js', 'index.tsx', 'index.jsx',
11
+ 'mod.rs', '__init__.pyi', 'types.ts', 'types.js', 'logger.ts', 'logger.js'
12
+ ]);
13
+
14
+ score(
15
+ edges: DependencyEdge[],
16
+ antiPatterns: AntiPattern[],
17
+ totalFiles: number
18
+ ): ArchitectureScore {
19
+ const modularity = this.calculateModularity(edges, totalFiles);
20
+ const coupling = this.calculateCoupling(edges, totalFiles);
21
+ const cohesion = this.calculateCohesion(edges);
22
+ const layering = this.calculateLayering(antiPatterns, totalFiles);
23
+
24
+ const components = [
25
+ {
26
+ name: 'Modularity',
27
+ score: Math.round(modularity),
28
+ maxScore: 100,
29
+ weight: 0.4,
30
+ explanation: 'Measures appropriate module boundaries and single responsibility principle adherence',
31
+ },
32
+ {
33
+ name: 'Coupling',
34
+ score: Math.round(coupling),
35
+ maxScore: 100,
36
+ weight: 0.25,
37
+ explanation: 'Evaluates interdependencies between modules; lower coupling is better',
38
+ },
39
+ {
40
+ name: 'Cohesion',
41
+ score: Math.round(cohesion),
42
+ maxScore: 100,
43
+ weight: 0.2,
44
+ explanation: 'Assesses how closely related functionality is grouped together',
45
+ },
46
+ {
47
+ name: 'Layering',
48
+ score: Math.round(layering),
49
+ maxScore: 100,
50
+ weight: 0.15,
51
+ explanation: 'Checks adherence to architectural layer separation',
52
+ },
53
+ ];
54
+
55
+ const overall = Math.round(
56
+ components[0].score * components[0].weight +
57
+ components[1].score * components[1].weight +
58
+ components[2].score * components[2].weight +
59
+ components[3].score * components[3].weight
60
+ );
61
+
62
+ return {
63
+ overall: Math.min(100, Math.max(0, overall)),
64
+ components,
65
+ breakdown: {
66
+ modularity: Math.round(modularity),
67
+ coupling: Math.round(coupling),
68
+ cohesion: Math.round(cohesion),
69
+ layering: Math.round(layering),
70
+ },
71
+ };
72
+ }
73
+
74
+ private calculateModularity(edges: DependencyEdge[], totalFiles: number): number {
75
+ if (totalFiles === 0) return 50;
76
+
77
+ const avgEdgesPerFile = edges.length / totalFiles;
78
+
79
+ if (avgEdgesPerFile < 2) return 100;
80
+ if (avgEdgesPerFile < 4) return 85;
81
+ if (avgEdgesPerFile < 6) return 70;
82
+ if (avgEdgesPerFile < 10) return 50;
83
+ return 30;
84
+ }
85
+
86
+ private calculateCoupling(edges: DependencyEdge[], totalFiles: number): number {
87
+ if (totalFiles === 0 || totalFiles === 1) return 50;
88
+
89
+ const nonBarrelEdges = edges.filter((e) => {
90
+ const fromFile = basename(e.from);
91
+ const toFile = basename(e.to);
92
+ return !ArchitectureScorer.BARREL_FILES.has(fromFile) &&
93
+ !ArchitectureScorer.BARREL_FILES.has(toFile);
94
+ });
95
+
96
+ const nodeWithMaxEdges = this.findNodeWithMaxEdges(nonBarrelEdges);
97
+ const maxEdgeCount = nodeWithMaxEdges ? nodeWithMaxEdges.count : 0;
98
+
99
+ const effectiveFiles = Math.max(totalFiles - 1, 1);
100
+ const couplingRatio = maxEdgeCount / effectiveFiles;
101
+
102
+ if (couplingRatio < 0.15) return 100;
103
+ if (couplingRatio < 0.25) return 85;
104
+ if (couplingRatio < 0.35) return 75;
105
+ if (couplingRatio < 0.5) return 65;
106
+ if (couplingRatio < 0.7) return 50;
107
+ if (couplingRatio < 0.85) return 35;
108
+ return 20;
109
+ }
110
+
111
+ private findNodeWithMaxEdges(edges: DependencyEdge[]): { node: string; count: number } | null {
112
+ const nodeEdgeCount: Record<string, number> = {};
113
+
114
+ for (const edge of edges) {
115
+ nodeEdgeCount[edge.from] = (nodeEdgeCount[edge.from] || 0) + 1;
116
+ nodeEdgeCount[edge.to] = (nodeEdgeCount[edge.to] || 0) + 1;
117
+ }
118
+
119
+ let maxNode: string | null = null;
120
+ let maxCount = 0;
121
+
122
+ for (const [node, count] of Object.entries(nodeEdgeCount)) {
123
+ if (count > maxCount) {
124
+ maxCount = count;
125
+ maxNode = node;
126
+ }
127
+ }
128
+
129
+ return maxNode ? { node: maxNode, count: maxCount } : null;
130
+ }
131
+
132
+ private calculateCohesion(edges: DependencyEdge[]): number {
133
+ if (edges.length === 0) return 50;
134
+
135
+ const internalEdges = edges.filter((e) =>
136
+ this.isInternalDependency(e.from, e.to)
137
+ ).length;
138
+
139
+ const cohesionRatio = internalEdges / edges.length;
140
+
141
+ if (cohesionRatio > 0.8) return 100;
142
+ if (cohesionRatio > 0.6) return 85;
143
+ if (cohesionRatio > 0.45) return 75;
144
+ if (cohesionRatio > 0.3) return 65;
145
+ if (cohesionRatio > 0.15) return 50;
146
+ return 30;
147
+ }
148
+
149
+ private isInternalDependency(from: string, to: string): boolean {
150
+ const fromParts = from.split('/');
151
+ const toParts = to.split('/');
152
+
153
+ if (fromParts.length <= 1 && toParts.length <= 1) return true;
154
+
155
+ const fromTopLevel = fromParts.length > 1 ? fromParts[0] : '';
156
+ const toTopLevel = toParts.length > 1 ? toParts[0] : '';
157
+
158
+ return fromTopLevel === toTopLevel;
159
+ }
160
+
161
+ private calculateLayering(antiPatterns: AntiPattern[], totalFiles?: number): number {
162
+ const layeringViolations = antiPatterns.filter(
163
+ (p) =>
164
+ p.name === 'Leaky Abstraction' ||
165
+ p.name === 'Shotgun Surgery' ||
166
+ p.name === 'Circular Dependency'
167
+ ).length;
168
+
169
+ if (layeringViolations === 0) return 100;
170
+
171
+ const fileCount = Math.max(totalFiles || 50, 10);
172
+ const violationRatio = layeringViolations / fileCount;
173
+
174
+ if (violationRatio < 0.05) return 95;
175
+ if (violationRatio < 0.15) return 85;
176
+ if (violationRatio < 0.25) return 70;
177
+ if (violationRatio < 0.40) return 50;
178
+ if (violationRatio < 0.60) return 35;
179
+ return 20;
180
+ }
181
+ }