@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,349 @@
1
+ import { globSync } from 'glob';
2
+ import { readFileSync, lstatSync, existsSync } from 'fs';
3
+ // @ts-ignore - Audit cleanup unused variable
4
+ import { join, relative, extname, resolve } from 'path';
5
+ import { FileNode, ProjectInfo, WorkspaceInfo } from '../core/types/infrastructure.js';
6
+ import { ArchitectConfig } from '../core/types/core.js';
7
+ import { logger } from './logger.js';
8
+
9
+ export class ProjectScanner {
10
+ private projectPath: string;
11
+ private config: ArchitectConfig;
12
+ // @ts-ignore - Audit cleanup unused variable
13
+ private frameworks: Set<string> = new Set();
14
+
15
+ constructor(projectPath: string, config: ArchitectConfig) {
16
+ this.projectPath = projectPath;
17
+ this.config = config;
18
+ }
19
+
20
+ scan(): ProjectInfo {
21
+ const files = this.scanDirectory();
22
+ const fileTree = this.buildFileTree(files);
23
+
24
+ // Detect workspaces from root package.json
25
+ const workspaces = this.detectWorkspaces();
26
+
27
+ // Detect frameworks ONLY from root + workspace package.json files (never from node_modules)
28
+ const workspacePkgJsonPaths = [
29
+ join(this.projectPath, 'package.json'),
30
+ ...workspaces.map(ws => join(ws.path, 'package.json')),
31
+ ].filter(p => existsSync(p));
32
+
33
+ const frameworks = this.detectFrameworks(workspacePkgJsonPaths);
34
+ const languages = this.detectLanguages(files);
35
+ const totalLines = this.countTotalLines(files);
36
+ const projectName = this.resolveProjectName(workspacePkgJsonPaths);
37
+
38
+ return {
39
+ path: this.projectPath,
40
+ name: projectName,
41
+ frameworks: Array.from(frameworks),
42
+ totalFiles: files.length,
43
+ totalLines,
44
+ primaryLanguages: languages,
45
+ fileTree,
46
+ workspaces: workspaces.length > 0 ? workspaces : undefined,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Detect npm/yarn/pnpm workspaces from root package.json.
52
+ * Reads the "workspaces" field and resolves each workspace to its package.json.
53
+ */
54
+ private detectWorkspaces(): WorkspaceInfo[] {
55
+ const rootPkgPath = join(this.projectPath, 'package.json');
56
+ if (!existsSync(rootPkgPath)) return [];
57
+
58
+ try {
59
+ const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
60
+ let workspaceGlobs: string[] = [];
61
+
62
+ if (Array.isArray(rootPkg.workspaces)) {
63
+ workspaceGlobs = rootPkg.workspaces;
64
+ } else if (rootPkg.workspaces?.packages && Array.isArray(rootPkg.workspaces.packages)) {
65
+ workspaceGlobs = rootPkg.workspaces.packages;
66
+ }
67
+
68
+ if (workspaceGlobs.length === 0) return [];
69
+
70
+ const workspaces: WorkspaceInfo[] = [];
71
+
72
+ for (const pattern of workspaceGlobs) {
73
+ // Resolve glob patterns like "packages/*"
74
+ const dirs = globSync(pattern, {
75
+ cwd: this.projectPath,
76
+ absolute: true,
77
+ });
78
+
79
+ for (const dir of dirs) {
80
+ const pkgPath = join(dir, 'package.json');
81
+ if (!existsSync(pkgPath)) continue;
82
+
83
+ try {
84
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
85
+ workspaces.push({
86
+ name: pkg.name || relative(this.projectPath, dir),
87
+ path: dir,
88
+ relativePath: relative(this.projectPath, dir),
89
+ description: pkg.description || '',
90
+ version: pkg.version || '0.0.0',
91
+ dependencies: pkg.dependencies || {},
92
+ devDependencies: pkg.devDependencies || {},
93
+ bin: pkg.bin || undefined,
94
+ main: pkg.main || undefined,
95
+ });
96
+ } catch (error) {
97
+ logger.debug('Skipping unparseable package.json in workspace', { pkgPath, error });
98
+ }
99
+ }
100
+ }
101
+
102
+ return workspaces;
103
+ } catch (error) {
104
+ logger.debug('Failed to parse root package.json for workspaces', { error });
105
+ return [];
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Resolve project name from nearest package.json or directory name
111
+ */
112
+ private resolveProjectName(packageJsonPaths: string[]): string {
113
+ for (const pkgPath of packageJsonPaths) {
114
+ try {
115
+ const content = readFileSync(pkgPath, 'utf-8');
116
+ const parsed = JSON.parse(content);
117
+ if (parsed.name) {
118
+ return parsed.name;
119
+ }
120
+ } catch (error) {
121
+ logger.debug('Skipping unparseable package.json while resolving project name', { pkgPath, error });
122
+ }
123
+ }
124
+ return this.projectPath.split('/').pop() || 'project';
125
+ }
126
+
127
+ private scanDirectory(): string[] {
128
+ const ignorePatterns = this.config.ignore || [];
129
+
130
+ const files = globSync('**/*', {
131
+ cwd: this.projectPath,
132
+ ignore: ignorePatterns,
133
+ absolute: true,
134
+ nodir: true,
135
+ });
136
+
137
+ return files.filter(
138
+ (f) =>
139
+ !lstatSync(f).isDirectory() && this.isSourceFile(f)
140
+ );
141
+ }
142
+
143
+ private isSourceFile(filePath: string): boolean {
144
+ const ext = extname(filePath).toLowerCase();
145
+ const sourceExtensions = [
146
+ '.js',
147
+ '.ts',
148
+ '.tsx',
149
+ '.jsx',
150
+ '.py',
151
+ '.java',
152
+ '.go',
153
+ '.rb',
154
+ '.php',
155
+ '.cs',
156
+ '.cpp',
157
+ '.c',
158
+ '.h',
159
+ '.rs',
160
+ '.kt',
161
+ '.scala',
162
+ '.groovy',
163
+ '.sql',
164
+ '.graphql',
165
+ '.json',
166
+ '.yaml',
167
+ '.yml',
168
+ '.xml',
169
+ ];
170
+ return sourceExtensions.includes(ext);
171
+ }
172
+
173
+ private buildFileTree(files: string[]): FileNode {
174
+ const root: FileNode = {
175
+ path: this.projectPath,
176
+ name: this.projectPath.split('/').pop() || 'root',
177
+ type: 'directory',
178
+ };
179
+
180
+ for (const file of files) {
181
+ const relativePath = relative(this.projectPath, file);
182
+ const parts = relativePath.split('/');
183
+ let current = root;
184
+
185
+ for (let i = 0; i < parts.length; i++) {
186
+ const part = parts[i];
187
+ const isFile = i === parts.length - 1;
188
+
189
+ let child = (current.children || []).find((c) => c.name === part);
190
+ if (!child) {
191
+ child = {
192
+ path: join(current.path, part),
193
+ name: part,
194
+ type: isFile ? 'file' : 'directory',
195
+ extension: isFile ? extname(part) : undefined,
196
+ };
197
+
198
+ if (isFile) {
199
+ child.lines = this.countLines(file);
200
+ child.language = this.detectLanguage(file);
201
+ }
202
+
203
+ if (!current.children) current.children = [];
204
+ current.children.push(child);
205
+ }
206
+
207
+ current = child;
208
+ }
209
+ }
210
+
211
+ return root;
212
+ }
213
+
214
+ /**
215
+ * Detect frameworks ONLY from specified package.json files.
216
+ * Never reads package.json from node_modules.
217
+ * No string-matching fallback — only structured dependency key detection.
218
+ */
219
+ private detectFrameworks(packageJsonPaths: string[]): Set<string> {
220
+ const frameworks = new Set<string>();
221
+
222
+ for (const file of packageJsonPaths) {
223
+ if (!file.endsWith('package.json')) continue;
224
+
225
+ // Safety: skip any path that includes node_modules
226
+ if (file.includes('node_modules')) continue;
227
+
228
+ try {
229
+ const content = readFileSync(file, 'utf-8');
230
+ const parsed = JSON.parse(content);
231
+ const allDeps = {
232
+ ...parsed.dependencies,
233
+ ...parsed.devDependencies,
234
+ };
235
+
236
+ // Detect from actual dependency keys — no string fallback
237
+ if (allDeps['@nestjs/core'] || allDeps['@nestjs/common']) frameworks.add('NestJS');
238
+ if (allDeps['react'] || allDeps['react-dom']) frameworks.add('React');
239
+ if (allDeps['@angular/core']) frameworks.add('Angular');
240
+ if (allDeps['vue'] || allDeps['@vue/core']) frameworks.add('Vue.js');
241
+ if (allDeps['express']) frameworks.add('Express.js');
242
+ if (allDeps['next']) frameworks.add('Next.js');
243
+ if (allDeps['fastify']) frameworks.add('Fastify');
244
+ if (allDeps['typeorm']) frameworks.add('TypeORM');
245
+ if (allDeps['prisma'] || allDeps['@prisma/client']) frameworks.add('Prisma');
246
+ if (allDeps['sequelize']) frameworks.add('Sequelize');
247
+ if (allDeps['mongoose']) frameworks.add('Mongoose');
248
+ if (allDeps['@modelcontextprotocol/sdk']) frameworks.add('MCP SDK');
249
+ if (allDeps['probot']) frameworks.add('Probot');
250
+ if (allDeps['hono']) frameworks.add('Hono');
251
+ } catch (error) {
252
+ logger.debug('Skipping unparseable package.json during framework detection', { file, error });
253
+ }
254
+ }
255
+
256
+ // Check for pom.xml only at project root
257
+ const pomPath = join(this.projectPath, 'pom.xml');
258
+ if (existsSync(pomPath)) {
259
+ try {
260
+ const content = readFileSync(pomPath, 'utf-8');
261
+ if (content.includes('spring-boot')) frameworks.add('Spring Boot');
262
+ if (content.includes('spring') && !content.includes('spring-boot')) frameworks.add('Spring');
263
+ } catch (error) {
264
+ logger.debug('Error reading pom.xml', { error });
265
+ }
266
+ }
267
+
268
+ // Check for requirements.txt only at project root
269
+ const reqPath = join(this.projectPath, 'requirements.txt');
270
+ if (existsSync(reqPath)) {
271
+ try {
272
+ const content = readFileSync(reqPath, 'utf-8');
273
+ if (content.includes('django')) frameworks.add('Django');
274
+ if (content.includes('flask')) frameworks.add('Flask');
275
+ if (content.includes('fastapi')) frameworks.add('FastAPI');
276
+ } catch (error) {
277
+ logger.debug('Error reading requirements.txt', { error });
278
+ }
279
+ }
280
+
281
+ // Check for Gemfile only at project root
282
+ const gemPath = join(this.projectPath, 'Gemfile');
283
+ if (existsSync(gemPath)) {
284
+ try {
285
+ const content = readFileSync(gemPath, 'utf-8');
286
+ if (content.includes('rails')) frameworks.add('Ruby on Rails');
287
+ } catch (error) {
288
+ logger.debug('Error reading Gemfile', { error });
289
+ }
290
+ }
291
+
292
+ // Check for go.mod only at project root
293
+ if (existsSync(join(this.projectPath, 'go.mod'))) {
294
+ frameworks.add('Go');
295
+ }
296
+
297
+ return frameworks;
298
+ }
299
+
300
+ private detectLanguages(files: string[]): string[] {
301
+ const languages = new Set<string>();
302
+
303
+ for (const file of files) {
304
+ const lang = this.detectLanguage(file);
305
+ if (lang && lang !== 'Unknown') languages.add(lang);
306
+ }
307
+
308
+ return Array.from(languages);
309
+ }
310
+
311
+ private detectLanguage(filePath: string): string {
312
+ const ext = extname(filePath).toLowerCase();
313
+
314
+ const languageMap: Record<string, string> = {
315
+ '.js': 'JavaScript',
316
+ '.ts': 'TypeScript',
317
+ '.tsx': 'TypeScript',
318
+ '.jsx': 'JavaScript',
319
+ '.py': 'Python',
320
+ '.java': 'Java',
321
+ '.go': 'Go',
322
+ '.rb': 'Ruby',
323
+ '.php': 'PHP',
324
+ '.cs': 'C#',
325
+ '.cpp': 'C++',
326
+ '.c': 'C',
327
+ '.rs': 'Rust',
328
+ '.kt': 'Kotlin',
329
+ '.scala': 'Scala',
330
+ '.sql': 'SQL',
331
+ '.graphql': 'GraphQL',
332
+ };
333
+
334
+ return languageMap[ext] || 'Unknown';
335
+ }
336
+
337
+ private countLines(filePath: string): number {
338
+ try {
339
+ const content = readFileSync(filePath, 'utf-8');
340
+ return content.split('\n').length;
341
+ } catch {
342
+ return 0;
343
+ }
344
+ }
345
+
346
+ private countTotalLines(files: string[]): number {
347
+ return files.reduce((total, file) => total + this.countLines(file), 0);
348
+ }
349
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Integration Tests — Full pipeline: GitHistory → TemporalScorer → ForecastEngine
3
+ *
4
+ * Validates end-to-end flow with a real git repo and type contracts
5
+ * between the three analyzer stages.
6
+ */
7
+
8
+ import { GitHistoryAnalyzer } from '../src/infrastructure/git-history.js';
9
+ import { TemporalScorer } from '../src/core/analyzers/temporal-scorer.js';
10
+ import { ForecastEngine } from '../src/core/analyzers/forecast.js';
11
+ import { saveToCache, loadFromCache } from '../src/infrastructure/git-cache.js';
12
+ import type { GitHistoryReport } from '../src/infrastructure/git-history.js';
13
+ import type { TemporalReport } from '../src/core/analyzers/temporal-scorer.js';
14
+ import type { WeatherForecast } from '../src/core/analyzers/forecast.js';
15
+ import { execSync } from 'child_process';
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+
19
+ // ═══════════════════════════════════════════════════════════════
20
+ // SETUP
21
+ // ═══════════════════════════════════════════════════════════════
22
+
23
+ const TEST_DIR = path.join('/tmp', 'architect-integration-test');
24
+
25
+ function setupRealRepo(): void {
26
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
27
+ fs.mkdirSync(TEST_DIR, { recursive: true });
28
+
29
+ execSync('git init', { cwd: TEST_DIR, stdio: 'pipe' });
30
+ execSync('git config user.email "dev@test.com"', { cwd: TEST_DIR, stdio: 'pipe' });
31
+ execSync('git config user.name "Dev"', { cwd: TEST_DIR, stdio: 'pipe' });
32
+
33
+ // Build a realistic repo with multiple modules
34
+ const files = [
35
+ { path: 'src/api/routes.ts', content: 'export const routes = [];' },
36
+ { path: 'src/api/middleware.ts', content: 'export function auth() {}' },
37
+ { path: 'src/service/user.ts', content: 'export class UserService {}' },
38
+ { path: 'src/data/repo.ts', content: 'export class UserRepo {}' },
39
+ { path: 'lib/utils.ts', content: 'export function log() {}' },
40
+ ];
41
+
42
+ for (const f of files) {
43
+ const dir = path.dirname(path.join(TEST_DIR, f.path));
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ fs.writeFileSync(path.join(TEST_DIR, f.path), f.content);
46
+ }
47
+ execSync('git add . && git commit -m "initial"', { cwd: TEST_DIR, stdio: 'pipe' });
48
+
49
+ // Simulate some activity
50
+ for (let i = 0; i < 5; i++) {
51
+ fs.writeFileSync(
52
+ path.join(TEST_DIR, 'src/api/routes.ts'),
53
+ `export const routes = [${i}];\n`.repeat(i + 1),
54
+ );
55
+ fs.writeFileSync(
56
+ path.join(TEST_DIR, 'src/service/user.ts'),
57
+ `export class UserService { v${i}() {} }`,
58
+ );
59
+ execSync(`git add . && git commit -m "iteration ${i}"`, { cwd: TEST_DIR, stdio: 'pipe' });
60
+ }
61
+ }
62
+
63
+ // ═══════════════════════════════════════════════════════════════
64
+ // TESTS
65
+ // ═══════════════════════════════════════════════════════════════
66
+
67
+ describe('Analyzer Pipeline Integration', () => {
68
+ let gitReport: GitHistoryReport;
69
+ let temporalReport: TemporalReport;
70
+ let forecast: WeatherForecast;
71
+
72
+ beforeAll(async () => {
73
+ setupRealRepo();
74
+
75
+ // Stage 1: Git History
76
+ const gitAnalyzer = new GitHistoryAnalyzer({ periodWeeks: 52 });
77
+ gitReport = await gitAnalyzer.analyze(TEST_DIR);
78
+
79
+ // Stage 2: Temporal Scoring
80
+ const scorer = new TemporalScorer({ projectionWeeks: 12 });
81
+ const staticScores = new Map<string, number>();
82
+ for (const mod of gitReport.modules) {
83
+ staticScores.set(mod.modulePath, 70); // assume 70 baseline
84
+ }
85
+ temporalReport = scorer.score(gitReport, staticScores);
86
+
87
+ // Stage 3: Forecast
88
+ const engine = new ForecastEngine();
89
+ forecast = engine.forecast(gitReport, temporalReport);
90
+ });
91
+
92
+ afterAll(() => {
93
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
94
+ });
95
+
96
+ describe('Stage 1 → Stage 2 contract', () => {
97
+ it('git report modules should have matching temporal scores', () => {
98
+ for (const mod of gitReport.modules) {
99
+ const ts = temporalReport.modules.find(m => m.module === mod.modulePath);
100
+ expect(ts).toBeDefined();
101
+ expect(ts!.staticScore).toBeDefined();
102
+ expect(ts!.temporalScore).toBeDefined();
103
+ }
104
+ });
105
+ });
106
+
107
+ describe('Stage 2 → Stage 3 contract', () => {
108
+ it('temporal modules should have matching forecast modules', () => {
109
+ for (const ts of temporalReport.modules) {
110
+ const fm = forecast.modules.find(m => m.module === ts.module);
111
+ expect(fm).toBeDefined();
112
+ expect(fm!.currentHealth).toMatch(/^(healthy|at-risk|degrading|critical)$/);
113
+ expect(fm!.forecast6Months).toMatch(/^(stable|declining|breakdown)$/);
114
+ }
115
+ });
116
+ });
117
+
118
+ describe('end-to-end output', () => {
119
+ it('forecast should have valid outlook', () => {
120
+ expect(['sunny', 'cloudy', 'stormy']).toContain(forecast.overallOutlook);
121
+ });
122
+
123
+ it('forecast should have a headline', () => {
124
+ expect(forecast.headline.length).toBeGreaterThan(0);
125
+ });
126
+
127
+ it('all pre-anti-patterns should have valid types', () => {
128
+ const validTypes = [
129
+ 'emerging-god-class',
130
+ 'emerging-shotgun-surgery',
131
+ 'emerging-feature-envy',
132
+ 'bus-factor-risk',
133
+ 'complexity-spiral',
134
+ 'coupling-magnet',
135
+ ];
136
+ for (const p of forecast.preAntiPatterns) {
137
+ expect(validTypes).toContain(p.type);
138
+ expect(p.confidence).toBeGreaterThan(0);
139
+ expect(p.confidence).toBeLessThanOrEqual(1);
140
+ }
141
+ });
142
+
143
+ it('modules should have bottleneck probability between 0 and 1', () => {
144
+ for (const m of forecast.modules) {
145
+ expect(m.bottleneckProbability).toBeGreaterThanOrEqual(0);
146
+ expect(m.bottleneckProbability).toBeLessThanOrEqual(1);
147
+ }
148
+ });
149
+ });
150
+
151
+ describe('git cache', () => {
152
+ it('should save and load cache correctly', () => {
153
+ saveToCache(gitReport, TEST_DIR);
154
+
155
+ const loaded = loadFromCache(TEST_DIR, '.architect-cache', 3600000);
156
+ expect(loaded).not.toBeNull();
157
+ expect(loaded!.projectPath).toBe(gitReport.projectPath);
158
+ expect(loaded!.totalCommits).toBe(gitReport.totalCommits);
159
+ expect(loaded!.totalAuthors).toBe(gitReport.totalAuthors);
160
+ });
161
+
162
+ it('should return null for expired cache', () => {
163
+ saveToCache(gitReport, TEST_DIR);
164
+
165
+ const loaded = loadFromCache(TEST_DIR, '.architect-cache', 0); // 0ms = expired
166
+ expect(loaded).toBeNull();
167
+ });
168
+
169
+ it('should return null when cache does not exist', () => {
170
+ const loaded = loadFromCache('/tmp/nonexistent-path');
171
+ expect(loaded).toBeNull();
172
+ });
173
+ });
174
+ });
@@ -0,0 +1,95 @@
1
+ import { AntiPatternDetector } from '../src/core/anti-patterns.js';
2
+ import { ArchitectConfig } from '../src/core/types/core.js';
3
+ import { FileNode } from '../src/core/types/infrastructure.js';
4
+
5
+ describe('AntiPatternDetector', () => {
6
+ const mockConfig: ArchitectConfig = {
7
+ antiPatterns: {
8
+ godClass: {
9
+ linesThreshold: 500,
10
+ methodsThreshold: 10,
11
+ },
12
+ shotgunSurgery: {
13
+ changePropagationThreshold: 5,
14
+ },
15
+ },
16
+ };
17
+
18
+ const mockFileTree: FileNode = {
19
+ path: '/project',
20
+ name: 'project',
21
+ type: 'directory',
22
+ children: [
23
+ {
24
+ path: '/project/src/services/UserManager.ts',
25
+ name: 'UserManager.ts',
26
+ type: 'file',
27
+ extension: '.ts',
28
+ lines: 850,
29
+ },
30
+ {
31
+ path: '/project/src/utils/helper.ts',
32
+ name: 'helper.ts',
33
+ type: 'file',
34
+ extension: '.ts',
35
+ lines: 200,
36
+ },
37
+ ],
38
+ };
39
+
40
+ const mockDependencies = new Map<string, Set<string>>([
41
+ ['/project/src/services/UserManager.ts', new Set(['/project/src/utils/helper.ts'])],
42
+ ['/project/src/utils/helper.ts', new Set(['/project/src/services/UserManager.ts'])],
43
+ ]);
44
+
45
+ describe('detect', () => {
46
+ it('should detect anti-patterns in code', async () => {
47
+ const detector = new AntiPatternDetector(mockConfig);
48
+ const patterns = await detector.detect(mockFileTree, mockDependencies);
49
+
50
+ expect(Array.isArray(patterns)).toBe(true);
51
+ });
52
+
53
+ it('should identify God Classes', async () => {
54
+ const detector = new AntiPatternDetector(mockConfig);
55
+ const patterns = await detector.detect(mockFileTree, mockDependencies);
56
+
57
+ const godClasses = patterns.filter((p) => p.name === 'God Class');
58
+ expect(godClasses.length).toBeGreaterThanOrEqual(0);
59
+ });
60
+
61
+ it('should sort patterns by severity', async () => {
62
+ const detector = new AntiPatternDetector(mockConfig);
63
+ const patterns = await detector.detect(mockFileTree, mockDependencies);
64
+
65
+ if (patterns.length > 1) {
66
+ const severityOrder: Record<string, number> = {
67
+ CRITICAL: 0,
68
+ HIGH: 1,
69
+ MEDIUM: 2,
70
+ LOW: 3,
71
+ };
72
+
73
+ for (let i = 0; i < patterns.length - 1; i++) {
74
+ const current = severityOrder[patterns[i].severity];
75
+ const next = severityOrder[patterns[i + 1].severity];
76
+ expect(current).toBeLessThanOrEqual(next);
77
+ }
78
+ }
79
+ });
80
+ });
81
+
82
+ describe('circular dependency detection', () => {
83
+ it('should detect circular dependencies', async () => {
84
+ const circularDeps = new Map<string, Set<string>>([
85
+ ['src/auth.ts', new Set(['src/cache.ts'])],
86
+ ['src/cache.ts', new Set(['src/auth.ts'])],
87
+ ]);
88
+
89
+ const detector = new AntiPatternDetector(mockConfig);
90
+ const patterns = await detector.detect(mockFileTree, circularDeps);
91
+
92
+ expect(patterns.length).toBeGreaterThanOrEqual(0);
93
+ });
94
+ });
95
+ });