@girardelli/architect 2.2.0 → 5.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.
Files changed (212) hide show
  1. package/README.md +105 -116
  2. package/architect-run.sh +431 -0
  3. package/assets/banner-v3.html +561 -0
  4. package/dist/agent-generator/context-enricher.d.ts +58 -0
  5. package/dist/agent-generator/context-enricher.d.ts.map +1 -0
  6. package/dist/agent-generator/context-enricher.js +613 -0
  7. package/dist/agent-generator/context-enricher.js.map +1 -0
  8. package/dist/agent-generator/domain-inferrer.d.ts +52 -0
  9. package/dist/agent-generator/domain-inferrer.d.ts.map +1 -0
  10. package/dist/agent-generator/domain-inferrer.js +585 -0
  11. package/dist/agent-generator/domain-inferrer.js.map +1 -0
  12. package/dist/agent-generator/framework-detector.d.ts +40 -0
  13. package/dist/agent-generator/framework-detector.d.ts.map +1 -0
  14. package/dist/agent-generator/framework-detector.js +611 -0
  15. package/dist/agent-generator/framework-detector.js.map +1 -0
  16. package/dist/agent-generator/index.d.ts +47 -0
  17. package/dist/agent-generator/index.d.ts.map +1 -0
  18. package/dist/agent-generator/index.js +545 -0
  19. package/dist/agent-generator/index.js.map +1 -0
  20. package/dist/agent-generator/stack-detector.d.ts +14 -0
  21. package/dist/agent-generator/stack-detector.d.ts.map +1 -0
  22. package/dist/agent-generator/stack-detector.js +124 -0
  23. package/dist/agent-generator/stack-detector.js.map +1 -0
  24. package/dist/agent-generator/templates/core/agents.d.ts +17 -0
  25. package/dist/agent-generator/templates/core/agents.d.ts.map +1 -0
  26. package/dist/agent-generator/templates/core/agents.js +1256 -0
  27. package/dist/agent-generator/templates/core/agents.js.map +1 -0
  28. package/dist/agent-generator/templates/core/architecture-rules.d.ts +7 -0
  29. package/dist/agent-generator/templates/core/architecture-rules.d.ts.map +1 -0
  30. package/dist/agent-generator/templates/core/architecture-rules.js +274 -0
  31. package/dist/agent-generator/templates/core/architecture-rules.js.map +1 -0
  32. package/dist/agent-generator/templates/core/general-rules.d.ts +8 -0
  33. package/dist/agent-generator/templates/core/general-rules.d.ts.map +1 -0
  34. package/dist/agent-generator/templates/core/general-rules.js +301 -0
  35. package/dist/agent-generator/templates/core/general-rules.js.map +1 -0
  36. package/dist/agent-generator/templates/core/hooks-generator.d.ts +21 -0
  37. package/dist/agent-generator/templates/core/hooks-generator.d.ts.map +1 -0
  38. package/dist/agent-generator/templates/core/hooks-generator.js +233 -0
  39. package/dist/agent-generator/templates/core/hooks-generator.js.map +1 -0
  40. package/dist/agent-generator/templates/core/index-md.d.ts +7 -0
  41. package/dist/agent-generator/templates/core/index-md.d.ts.map +1 -0
  42. package/dist/agent-generator/templates/core/index-md.js +246 -0
  43. package/dist/agent-generator/templates/core/index-md.js.map +1 -0
  44. package/dist/agent-generator/templates/core/orchestrator.d.ts +8 -0
  45. package/dist/agent-generator/templates/core/orchestrator.d.ts.map +1 -0
  46. package/dist/agent-generator/templates/core/orchestrator.js +422 -0
  47. package/dist/agent-generator/templates/core/orchestrator.js.map +1 -0
  48. package/dist/agent-generator/templates/core/preflight.d.ts +8 -0
  49. package/dist/agent-generator/templates/core/preflight.d.ts.map +1 -0
  50. package/dist/agent-generator/templates/core/preflight.js +213 -0
  51. package/dist/agent-generator/templates/core/preflight.js.map +1 -0
  52. package/dist/agent-generator/templates/core/quality-gates.d.ts +11 -0
  53. package/dist/agent-generator/templates/core/quality-gates.d.ts.map +1 -0
  54. package/dist/agent-generator/templates/core/quality-gates.js +254 -0
  55. package/dist/agent-generator/templates/core/quality-gates.js.map +1 -0
  56. package/dist/agent-generator/templates/core/security-rules.d.ts +7 -0
  57. package/dist/agent-generator/templates/core/security-rules.d.ts.map +1 -0
  58. package/dist/agent-generator/templates/core/security-rules.js +528 -0
  59. package/dist/agent-generator/templates/core/security-rules.js.map +1 -0
  60. package/dist/agent-generator/templates/core/skills-generator.d.ts +19 -0
  61. package/dist/agent-generator/templates/core/skills-generator.d.ts.map +1 -0
  62. package/dist/agent-generator/templates/core/skills-generator.js +546 -0
  63. package/dist/agent-generator/templates/core/skills-generator.js.map +1 -0
  64. package/dist/agent-generator/templates/core/workflow-fix-bug.d.ts +7 -0
  65. package/dist/agent-generator/templates/core/workflow-fix-bug.d.ts.map +1 -0
  66. package/dist/agent-generator/templates/core/workflow-fix-bug.js +237 -0
  67. package/dist/agent-generator/templates/core/workflow-fix-bug.js.map +1 -0
  68. package/dist/agent-generator/templates/core/workflow-new-feature.d.ts +8 -0
  69. package/dist/agent-generator/templates/core/workflow-new-feature.d.ts.map +1 -0
  70. package/dist/agent-generator/templates/core/workflow-new-feature.js +321 -0
  71. package/dist/agent-generator/templates/core/workflow-new-feature.js.map +1 -0
  72. package/dist/agent-generator/templates/core/workflow-review.d.ts +7 -0
  73. package/dist/agent-generator/templates/core/workflow-review.d.ts.map +1 -0
  74. package/dist/agent-generator/templates/core/workflow-review.js +104 -0
  75. package/dist/agent-generator/templates/core/workflow-review.js.map +1 -0
  76. package/dist/agent-generator/templates/domain/index.d.ts +22 -0
  77. package/dist/agent-generator/templates/domain/index.d.ts.map +1 -0
  78. package/dist/agent-generator/templates/domain/index.js +1176 -0
  79. package/dist/agent-generator/templates/domain/index.js.map +1 -0
  80. package/dist/agent-generator/templates/stack/index.d.ts +8 -0
  81. package/dist/agent-generator/templates/stack/index.d.ts.map +1 -0
  82. package/dist/agent-generator/templates/stack/index.js +695 -0
  83. package/dist/agent-generator/templates/stack/index.js.map +1 -0
  84. package/dist/agent-generator/templates/template-helpers.d.ts +75 -0
  85. package/dist/agent-generator/templates/template-helpers.d.ts.map +1 -0
  86. package/dist/agent-generator/templates/template-helpers.js +726 -0
  87. package/dist/agent-generator/templates/template-helpers.js.map +1 -0
  88. package/dist/agent-generator/types.d.ts +196 -0
  89. package/dist/agent-generator/types.d.ts.map +1 -0
  90. package/dist/agent-generator/types.js +27 -0
  91. package/dist/agent-generator/types.js.map +1 -0
  92. package/dist/analyzer.d.ts +5 -0
  93. package/dist/analyzer.d.ts.map +1 -1
  94. package/dist/analyzer.js +46 -5
  95. package/dist/analyzer.js.map +1 -1
  96. package/dist/analyzers/forecast.d.ts +85 -0
  97. package/dist/analyzers/forecast.d.ts.map +1 -0
  98. package/dist/analyzers/forecast.js +337 -0
  99. package/dist/analyzers/forecast.js.map +1 -0
  100. package/dist/analyzers/git-cache.d.ts +7 -0
  101. package/dist/analyzers/git-cache.d.ts.map +1 -0
  102. package/dist/analyzers/git-cache.js +41 -0
  103. package/dist/analyzers/git-cache.js.map +1 -0
  104. package/dist/analyzers/git-history.d.ts +113 -0
  105. package/dist/analyzers/git-history.d.ts.map +1 -0
  106. package/dist/analyzers/git-history.js +333 -0
  107. package/dist/analyzers/git-history.js.map +1 -0
  108. package/dist/analyzers/index.d.ts +10 -0
  109. package/dist/analyzers/index.d.ts.map +1 -0
  110. package/dist/analyzers/index.js +7 -0
  111. package/dist/analyzers/index.js.map +1 -0
  112. package/dist/analyzers/temporal-scorer.d.ts +72 -0
  113. package/dist/analyzers/temporal-scorer.d.ts.map +1 -0
  114. package/dist/analyzers/temporal-scorer.js +140 -0
  115. package/dist/analyzers/temporal-scorer.js.map +1 -0
  116. package/dist/anti-patterns.d.ts +7 -0
  117. package/dist/anti-patterns.d.ts.map +1 -1
  118. package/dist/anti-patterns.js +25 -6
  119. package/dist/anti-patterns.js.map +1 -1
  120. package/dist/cli.d.ts +2 -3
  121. package/dist/cli.d.ts.map +1 -1
  122. package/dist/cli.js +275 -113
  123. package/dist/cli.js.map +1 -1
  124. package/dist/config.d.ts +6 -0
  125. package/dist/config.d.ts.map +1 -1
  126. package/dist/config.js +48 -11
  127. package/dist/config.js.map +1 -1
  128. package/dist/html-reporter.d.ts +3 -1
  129. package/dist/html-reporter.d.ts.map +1 -1
  130. package/dist/html-reporter.js +248 -12
  131. package/dist/html-reporter.js.map +1 -1
  132. package/dist/index.d.ts +16 -3
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +63 -4
  135. package/dist/index.js.map +1 -1
  136. package/dist/project-summarizer.d.ts +38 -0
  137. package/dist/project-summarizer.d.ts.map +1 -0
  138. package/dist/project-summarizer.js +463 -0
  139. package/dist/project-summarizer.js.map +1 -0
  140. package/dist/refactor-reporter.js +1 -1
  141. package/dist/scanner.d.ts +8 -2
  142. package/dist/scanner.d.ts.map +1 -1
  143. package/dist/scanner.js +153 -113
  144. package/dist/scanner.js.map +1 -1
  145. package/dist/scorer.d.ts.map +1 -1
  146. package/dist/scorer.js +24 -11
  147. package/dist/scorer.js.map +1 -1
  148. package/dist/types.d.ts +29 -0
  149. package/dist/types.d.ts.map +1 -1
  150. package/package.json +12 -3
  151. package/src/agent-generator/context-enricher.ts +672 -0
  152. package/src/agent-generator/domain-inferrer.ts +635 -0
  153. package/src/agent-generator/framework-detector.ts +669 -0
  154. package/src/agent-generator/index.ts +634 -0
  155. package/src/agent-generator/stack-detector.ts +115 -0
  156. package/src/agent-generator/templates/core/agents.ts +1296 -0
  157. package/src/agent-generator/templates/core/architecture-rules.ts +287 -0
  158. package/src/agent-generator/templates/core/general-rules.ts +306 -0
  159. package/src/agent-generator/templates/core/hooks-generator.ts +242 -0
  160. package/src/agent-generator/templates/core/index-md.ts +260 -0
  161. package/src/agent-generator/templates/core/orchestrator.ts +459 -0
  162. package/src/agent-generator/templates/core/preflight.ts +215 -0
  163. package/src/agent-generator/templates/core/quality-gates.ts +256 -0
  164. package/src/agent-generator/templates/core/security-rules.ts +543 -0
  165. package/src/agent-generator/templates/core/skills-generator.ts +585 -0
  166. package/src/agent-generator/templates/core/workflow-fix-bug.ts +239 -0
  167. package/src/agent-generator/templates/core/workflow-new-feature.ts +323 -0
  168. package/src/agent-generator/templates/core/workflow-review.ts +106 -0
  169. package/src/agent-generator/templates/domain/index.ts +1201 -0
  170. package/src/agent-generator/templates/stack/index.ts +705 -0
  171. package/src/agent-generator/templates/template-helpers.ts +776 -0
  172. package/src/agent-generator/types.ts +232 -0
  173. package/src/analyzer.ts +51 -5
  174. package/src/analyzers/forecast.ts +496 -0
  175. package/src/analyzers/git-cache.ts +52 -0
  176. package/src/analyzers/git-history.ts +488 -0
  177. package/src/analyzers/index.ts +33 -0
  178. package/src/analyzers/temporal-scorer.ts +227 -0
  179. package/src/anti-patterns.ts +29 -6
  180. package/src/cli.ts +316 -117
  181. package/src/config.ts +52 -11
  182. package/src/html-reporter.ts +263 -13
  183. package/src/index.ts +93 -10
  184. package/src/project-summarizer.ts +521 -0
  185. package/src/refactor-reporter.ts +1 -1
  186. package/src/scanner.ts +136 -90
  187. package/src/scorer.ts +26 -11
  188. package/src/types.ts +27 -0
  189. package/tests/agent-generator.test.ts +427 -0
  190. package/tests/analyzers-integration.test.ts +174 -0
  191. package/tests/architect-adapter-enrichment.test.ts +9 -0
  192. package/tests/context-enricher.test.ts +971 -0
  193. package/tests/fixtures/monorepo/package.json +6 -0
  194. package/tests/fixtures/monorepo/packages/app/package.json +12 -0
  195. package/tests/fixtures/monorepo/packages/app/src/index.ts +6 -0
  196. package/tests/fixtures/monorepo/packages/core/package.json +7 -0
  197. package/tests/fixtures/monorepo/packages/core/src/index.ts +7 -0
  198. package/tests/forecast.test.ts +509 -0
  199. package/tests/framework-detector.test.ts +1172 -0
  200. package/tests/git-history.test.ts +254 -0
  201. package/tests/monorepo-scan.test.ts +170 -0
  202. package/tests/scanner.test.ts +7 -8
  203. package/tests/scorer.test.ts +594 -0
  204. package/tests/stack-detector.test.ts +241 -0
  205. package/tests/template-generation.test.ts +706 -0
  206. package/tests/template-helpers.test.ts +1152 -0
  207. package/tests/temporal-scorer.test.ts +307 -0
  208. package/dist/agent-generator.d.ts +0 -106
  209. package/dist/agent-generator.d.ts.map +0 -1
  210. package/dist/agent-generator.js +0 -1398
  211. package/dist/agent-generator.js.map +0 -1
  212. package/src/agent-generator.ts +0 -1526
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Tests for GitHistoryAnalyzer
3
+ *
4
+ * Validates git log parsing, file history building, velocity vectors,
5
+ * change coupling detection, and timeline construction.
6
+ */
7
+
8
+ import { GitHistoryAnalyzer } from '../src/analyzers/git-history.js';
9
+ import type {
10
+ GitHistoryReport,
11
+ FileHistory,
12
+ ModuleHistory,
13
+ VelocityVector,
14
+ } from '../src/analyzers/git-history.js';
15
+ import { execSync } from 'child_process';
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+
19
+ // ═══════════════════════════════════════════════════════════════
20
+ // TEST HELPERS
21
+ // ═══════════════════════════════════════════════════════════════
22
+
23
+ const TEST_DIR = path.join('/tmp', 'architect-git-history-test');
24
+
25
+ function setupGitRepo(): 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 "test@test.com"', { cwd: TEST_DIR, stdio: 'pipe' });
31
+ execSync('git config user.name "TestUser"', { cwd: TEST_DIR, stdio: 'pipe' });
32
+ }
33
+
34
+ function commitFile(
35
+ filePath: string,
36
+ content: string,
37
+ message: string,
38
+ author = 'TestUser <test@test.com>',
39
+ ): void {
40
+ const fullPath = path.join(TEST_DIR, filePath);
41
+ const dir = path.dirname(fullPath);
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ fs.writeFileSync(fullPath, content);
44
+ execSync(`git add "${filePath}"`, { cwd: TEST_DIR, stdio: 'pipe' });
45
+ execSync(`git commit --author="${author}" -m "${message}"`, {
46
+ cwd: TEST_DIR,
47
+ stdio: 'pipe',
48
+ });
49
+ }
50
+
51
+ function cleanupRepo(): void {
52
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
53
+ }
54
+
55
+ // ═══════════════════════════════════════════════════════════════
56
+ // UNIT TESTS — parseLogOutput (via analyze)
57
+ // ═══════════════════════════════════════════════════════════════
58
+
59
+ describe('GitHistoryAnalyzer', () => {
60
+ beforeAll(() => {
61
+ setupGitRepo();
62
+
63
+ // Create initial files across two modules
64
+ commitFile('src/core.ts', 'export class Core {}', 'initial core', 'Alice <alice@test.com>');
65
+ commitFile('src/utils.ts', 'export function util() {}', 'add utils', 'Alice <alice@test.com>');
66
+ commitFile('lib/helper.ts', 'export function help() {}', 'add helper', 'Bob <bob@test.com>');
67
+
68
+ // Add more changes to create churn
69
+ commitFile('src/core.ts', 'export class Core { run() {} }', 'enhance core', 'Bob <bob@test.com>');
70
+ commitFile('src/core.ts', 'export class Core { run() { return 1; } }', 'fix core', 'Alice <alice@test.com>');
71
+
72
+ // Concurrent changes for coupling detection
73
+ const coupledContent1 = 'export class Core { run() { return 2; } }';
74
+ const coupledContent2 = 'export function util() { return true; }';
75
+ const p1 = path.join(TEST_DIR, 'src/core.ts');
76
+ const p2 = path.join(TEST_DIR, 'src/utils.ts');
77
+ fs.writeFileSync(p1, coupledContent1);
78
+ fs.writeFileSync(p2, coupledContent2);
79
+ execSync('git add .', { cwd: TEST_DIR, stdio: 'pipe' });
80
+ execSync('git commit --author="Alice <alice@test.com>" -m "coupled change 1"', {
81
+ cwd: TEST_DIR,
82
+ stdio: 'pipe',
83
+ });
84
+
85
+ // Another coupled change
86
+ fs.writeFileSync(p1, coupledContent1 + '\n// v2');
87
+ fs.writeFileSync(p2, coupledContent2 + '\n// v2');
88
+ execSync('git add .', { cwd: TEST_DIR, stdio: 'pipe' });
89
+ execSync('git commit --author="Bob <bob@test.com>" -m "coupled change 2"', {
90
+ cwd: TEST_DIR,
91
+ stdio: 'pipe',
92
+ });
93
+
94
+ // Third coupled change to meet the default minCochanges=3
95
+ fs.writeFileSync(p1, coupledContent1 + '\n// v3');
96
+ fs.writeFileSync(p2, coupledContent2 + '\n// v3');
97
+ execSync('git add .', { cwd: TEST_DIR, stdio: 'pipe' });
98
+ execSync('git commit --author="Alice <alice@test.com>" -m "coupled change 3"', {
99
+ cwd: TEST_DIR,
100
+ stdio: 'pipe',
101
+ });
102
+ });
103
+
104
+ afterAll(() => {
105
+ cleanupRepo();
106
+ });
107
+
108
+ describe('analyze()', () => {
109
+ let report: GitHistoryReport;
110
+
111
+ beforeAll(() => {
112
+ const analyzer = new GitHistoryAnalyzer({ periodWeeks: 52 });
113
+ report = analyzer.analyze(TEST_DIR);
114
+ });
115
+
116
+ it('should return correct project path', () => {
117
+ expect(report.projectPath).toBe(TEST_DIR);
118
+ });
119
+
120
+ it('should have analyzedAt timestamp', () => {
121
+ expect(report.analyzedAt).toBeDefined();
122
+ expect(new Date(report.analyzedAt).getTime()).not.toBeNaN();
123
+ });
124
+
125
+ it('should count total commits', () => {
126
+ expect(report.totalCommits).toBeGreaterThanOrEqual(7);
127
+ });
128
+
129
+ it('should count unique authors', () => {
130
+ expect(report.totalAuthors).toBe(2); // Alice + Bob
131
+ });
132
+
133
+ it('should detect modules by first directory level', () => {
134
+ const moduleNames = report.modules.map(m => m.modulePath);
135
+ expect(moduleNames).toContain('src');
136
+ expect(moduleNames).toContain('lib');
137
+ });
138
+
139
+ it('should build file histories with correct metrics', () => {
140
+ const srcModule = report.modules.find(m => m.modulePath === 'src');
141
+ expect(srcModule).toBeDefined();
142
+
143
+ const coreFile = srcModule!.files.find(f => f.path === 'src/core.ts');
144
+ expect(coreFile).toBeDefined();
145
+ expect(coreFile!.commits).toBeGreaterThanOrEqual(5);
146
+ expect(coreFile!.busFactor).toBe(2); // Alice + Bob
147
+ expect(coreFile!.totalAdditions).toBeGreaterThan(0);
148
+ });
149
+
150
+ it('should calculate churn rate correctly', () => {
151
+ const srcModule = report.modules.find(m => m.modulePath === 'src');
152
+ const coreFile = srcModule!.files.find(f => f.path === 'src/core.ts');
153
+ expect(coreFile!.churnRate).toBeGreaterThan(0);
154
+ expect(coreFile!.churnRate).toBe(
155
+ (coreFile!.totalAdditions + coreFile!.totalDeletions) / coreFile!.commits
156
+ );
157
+ });
158
+
159
+ it('should build weekly timeline', () => {
160
+ expect(report.commitTimeline.length).toBeGreaterThan(0);
161
+ const total = report.commitTimeline.reduce((s, w) => s + w.commits, 0);
162
+ expect(total).toBe(report.totalCommits);
163
+ });
164
+ });
165
+
166
+ describe('change coupling', () => {
167
+ it('should detect coupled files', () => {
168
+ const analyzer = new GitHistoryAnalyzer({ periodWeeks: 52, couplingMinCochanges: 3 });
169
+ const report = analyzer.analyze(TEST_DIR);
170
+
171
+ const coupling = report.changeCouplings.find(
172
+ c =>
173
+ (c.fileA === 'src/core.ts' && c.fileB === 'src/utils.ts') ||
174
+ (c.fileA === 'src/utils.ts' && c.fileB === 'src/core.ts')
175
+ );
176
+
177
+ expect(coupling).toBeDefined();
178
+ expect(coupling!.cochangeCount).toBeGreaterThanOrEqual(3);
179
+ expect(coupling!.confidence).toBeGreaterThan(0);
180
+ expect(coupling!.confidence).toBeLessThanOrEqual(1);
181
+ });
182
+ });
183
+
184
+ describe('velocity vectors', () => {
185
+ it('should calculate velocity for each module', () => {
186
+ const analyzer = new GitHistoryAnalyzer({ periodWeeks: 52 });
187
+ const report = analyzer.analyze(TEST_DIR);
188
+
189
+ const srcModule = report.modules.find(m => m.modulePath === 'src');
190
+ expect(srcModule).toBeDefined();
191
+ expect(srcModule!.velocityVector).toBeDefined();
192
+ expect(srcModule!.velocityVector.direction).toMatch(/^(accelerating|stable|decelerating)$/);
193
+ expect(typeof srcModule!.velocityVector.commitAcceleration).toBe('number');
194
+ expect(typeof srcModule!.velocityVector.churnTrend).toBe('number');
195
+ });
196
+ });
197
+
198
+ describe('module aggregation', () => {
199
+ it('should aggregate commits across module files', () => {
200
+ const analyzer = new GitHistoryAnalyzer({ periodWeeks: 52 });
201
+ const report = analyzer.analyze(TEST_DIR);
202
+
203
+ const srcModule = report.modules.find(m => m.modulePath === 'src');
204
+ expect(srcModule!.aggregateCommits).toBe(
205
+ srcModule!.files.reduce((s, f) => s + f.commits, 0)
206
+ );
207
+ });
208
+
209
+ it('should calculate bus factor per module', () => {
210
+ const analyzer = new GitHistoryAnalyzer({ periodWeeks: 52 });
211
+ const report = analyzer.analyze(TEST_DIR);
212
+
213
+ const srcModule = report.modules.find(m => m.modulePath === 'src');
214
+ expect(srcModule!.busFactor).toBe(2); // Alice + Bob both committed to src/
215
+ });
216
+
217
+ it('should sort modules by aggregate churn descending', () => {
218
+ const analyzer = new GitHistoryAnalyzer({ periodWeeks: 52 });
219
+ const report = analyzer.analyze(TEST_DIR);
220
+
221
+ for (let i = 1; i < report.modules.length; i++) {
222
+ expect(report.modules[i - 1].aggregateChurn).toBeGreaterThanOrEqual(
223
+ report.modules[i].aggregateChurn
224
+ );
225
+ }
226
+ });
227
+ });
228
+
229
+ describe('error handling', () => {
230
+ it('should throw for non-git directory', () => {
231
+ const tmpDir = path.join('/tmp', 'not-a-git-repo');
232
+ fs.mkdirSync(tmpDir, { recursive: true });
233
+
234
+ const analyzer = new GitHistoryAnalyzer();
235
+ expect(() => analyzer.analyze(tmpDir)).toThrow('Not a git repository');
236
+
237
+ fs.rmSync(tmpDir, { recursive: true, force: true });
238
+ });
239
+ });
240
+
241
+ describe('configuration', () => {
242
+ it('should respect custom period weeks', () => {
243
+ const analyzer = new GitHistoryAnalyzer({ periodWeeks: 1 });
244
+ const report = analyzer.analyze(TEST_DIR);
245
+ expect(report.periodWeeks).toBe(1);
246
+ });
247
+
248
+ it('should respect custom coupling threshold', () => {
249
+ const analyzer = new GitHistoryAnalyzer({ periodWeeks: 52, couplingMinCochanges: 100 });
250
+ const report = analyzer.analyze(TEST_DIR);
251
+ expect(report.changeCouplings.length).toBe(0);
252
+ });
253
+ });
254
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * E2E Tests — Monorepo Scanning
3
+ *
4
+ * Ensures that the Architect scanner correctly handles monorepo projects
5
+ * with nested node_modules, workspace detection, and framework inference.
6
+ *
7
+ * This test suite was created to prevent regression of the critical bug where
8
+ * node_modules in sub-packages leaked through the glob ignore filter,
9
+ * contaminating all analysis results (29,181 files instead of 245).
10
+ */
11
+
12
+ import path from 'path';
13
+ import { ProjectScanner } from '../src/scanner.js';
14
+ import { ArchitectureAnalyzer } from '../src/analyzer.js';
15
+ import { AntiPatternDetector } from '../src/anti-patterns.js';
16
+ import { ConfigLoader, normalizeIgnorePatterns } from '../src/config.js';
17
+
18
+ // Use path.resolve for Jest compatibility (import.meta.url not supported by ts-jest)
19
+ const FIXTURE_PATH = path.resolve(process.cwd(), 'tests/fixtures/monorepo');
20
+
21
+ describe('Monorepo Scanner — node_modules exclusion', () => {
22
+ let config: ReturnType<typeof ConfigLoader.loadConfig>;
23
+
24
+ beforeAll(() => {
25
+ config = ConfigLoader.loadConfig(FIXTURE_PATH);
26
+ });
27
+
28
+ test('default config uses glob patterns for ignore', () => {
29
+ expect(config.ignore).toBeDefined();
30
+ // Must use glob-style patterns that cover nested directories
31
+ const hasGlobNodeModules = config.ignore!.some(p => p.includes('**/node_modules'));
32
+ expect(hasGlobNodeModules).toBe(true);
33
+ });
34
+
35
+ test('scans only project source files, not node_modules', () => {
36
+ const scanner = new ProjectScanner(FIXTURE_PATH, config);
37
+ const result = scanner.scan();
38
+
39
+ // Only 2 source .ts files exist: core/src/index.ts and app/src/index.ts
40
+ // Plus 3 package.json files (root + core + app)
41
+ const tsFiles = result.totalFiles;
42
+
43
+ // node_modules has 4 files (fake-dep/index.js, fake-dep/package.json,
44
+ // another-dep/index.js, root-dep/index.js) — NONE should appear
45
+ expect(tsFiles).toBeLessThanOrEqual(10); // generous upper bound
46
+ expect(tsFiles).toBeGreaterThanOrEqual(2); // at least the 2 .ts files
47
+
48
+ // Verify no node_modules paths in the file tree
49
+ const allPaths = getAllFilePaths(result.fileTree!);
50
+ const nmPaths = allPaths.filter(p => p.includes('node_modules'));
51
+ expect(nmPaths).toHaveLength(0);
52
+ });
53
+
54
+ test('detects workspaces from root package.json', () => {
55
+ const scanner = new ProjectScanner(FIXTURE_PATH, config);
56
+ const result = scanner.scan();
57
+
58
+ expect(result.workspaces).toBeDefined();
59
+ expect(result.workspaces!.length).toBe(2);
60
+
61
+ const names = result.workspaces!.map(ws => ws.name);
62
+ expect(names).toContain('@test/core');
63
+ expect(names).toContain('@test/app');
64
+ });
65
+
66
+ test('detects Express.js from workspace package.json (not from node_modules)', () => {
67
+ const scanner = new ProjectScanner(FIXTURE_PATH, config);
68
+ const result = scanner.scan();
69
+
70
+ expect(result.frameworks).toContain('Express.js');
71
+ });
72
+
73
+ test('does NOT detect Vue.js or Mongoose from nested node_modules', () => {
74
+ const scanner = new ProjectScanner(FIXTURE_PATH, config);
75
+ const result = scanner.scan();
76
+
77
+ // fake-dep/package.json has vue and mongoose as deps
78
+ // These must NOT appear as project frameworks
79
+ expect(result.frameworks).not.toContain('Vue.js');
80
+ expect(result.frameworks).not.toContain('Mongoose');
81
+ });
82
+
83
+ test('does NOT detect Spring Boot or Spring (no Java in project)', () => {
84
+ const scanner = new ProjectScanner(FIXTURE_PATH, config);
85
+ const result = scanner.scan();
86
+
87
+ expect(result.frameworks).not.toContain('Spring Boot');
88
+ expect(result.frameworks).not.toContain('Spring');
89
+ });
90
+
91
+ test('dependency graph contains no node_modules paths', () => {
92
+ const scanner = new ProjectScanner(FIXTURE_PATH, config);
93
+ const result = scanner.scan();
94
+
95
+ const analyzer = new ArchitectureAnalyzer(FIXTURE_PATH);
96
+ const edges = analyzer.analyzeDependencies(result.fileTree!);
97
+
98
+ for (const edge of edges) {
99
+ expect(edge.from).not.toContain('node_modules');
100
+ expect(edge.to).not.toContain('node_modules');
101
+ }
102
+ });
103
+
104
+ test('anti-patterns are only from project files', () => {
105
+ const scanner = new ProjectScanner(FIXTURE_PATH, config);
106
+ const result = scanner.scan();
107
+
108
+ const analyzer = new ArchitectureAnalyzer(FIXTURE_PATH);
109
+ const edges = analyzer.analyzeDependencies(result.fileTree!);
110
+ const deps = new Map<string, Set<string>>();
111
+ for (const edge of edges) {
112
+ if (!deps.has(edge.from)) deps.set(edge.from, new Set());
113
+ deps.get(edge.from)!.add(edge.to);
114
+ }
115
+
116
+ const detector = new AntiPatternDetector(config);
117
+ const patterns = detector.detect(result.fileTree!, deps);
118
+
119
+ for (const pattern of patterns) {
120
+ expect(pattern.location).not.toContain('node_modules');
121
+ if (pattern.affectedFiles) {
122
+ for (const file of pattern.affectedFiles) {
123
+ expect(file).not.toContain('node_modules');
124
+ }
125
+ }
126
+ }
127
+ });
128
+ });
129
+
130
+ describe('normalizeIgnorePatterns', () => {
131
+ test('converts simple names to glob patterns', () => {
132
+ const result = normalizeIgnorePatterns(['node_modules']);
133
+ expect(result).toContain('**/node_modules/**');
134
+ expect(result).toContain('**/node_modules');
135
+ expect(result).toContain('node_modules/**');
136
+ });
137
+
138
+ test('preserves existing glob patterns', () => {
139
+ const result = normalizeIgnorePatterns(['**/dist/**']);
140
+ expect(result).toContain('**/dist/**');
141
+ // Should not double-wrap
142
+ expect(result).not.toContain('**/**/dist/**/**');
143
+ });
144
+
145
+ test('handles mixed patterns', () => {
146
+ const result = normalizeIgnorePatterns(['node_modules', '**/custom/**', 'dist']);
147
+ expect(result).toContain('**/node_modules/**');
148
+ expect(result).toContain('**/custom/**');
149
+ expect(result).toContain('**/dist/**');
150
+ });
151
+
152
+ test('handles path-like patterns with slashes', () => {
153
+ const result = normalizeIgnorePatterns(['some/specific/path']);
154
+ expect(result).toContain('some/specific/path');
155
+ // Should not expand patterns with slashes
156
+ expect(result).not.toContain('**/some/specific/path/**');
157
+ });
158
+ });
159
+
160
+ // Helper
161
+ function getAllFilePaths(node: { path: string; type: string; children?: any[] }): string[] {
162
+ const paths: string[] = [];
163
+ if (node.type === 'file') paths.push(node.path);
164
+ if (node.children) {
165
+ for (const child of node.children) {
166
+ paths.push(...getAllFilePaths(child));
167
+ }
168
+ }
169
+ return paths;
170
+ }
@@ -1,10 +1,9 @@
1
- import { fileURLToPath } from 'url';
2
1
  import path from 'path';
3
2
  import { ProjectScanner } from '../src/scanner.js';
4
3
  import { ArchitectConfig } from '../src/types.js';
5
4
 
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
5
+ // Use path.resolve for Jest compatibility (import.meta.url not supported by ts-jest)
6
+ const testDir = path.resolve(process.cwd(), 'tests');
8
7
 
9
8
  describe('ProjectScanner', () => {
10
9
  const mockConfig: ArchitectConfig = {
@@ -14,11 +13,11 @@ describe('ProjectScanner', () => {
14
13
 
15
14
  describe('scan', () => {
16
15
  it('should scan a project directory and return project info', () => {
17
- const scanner = new ProjectScanner(__dirname, mockConfig);
16
+ const scanner = new ProjectScanner(testDir, mockConfig);
18
17
  const info = scanner.scan();
19
18
 
20
19
  expect(info).toBeDefined();
21
- expect(info.path).toBe(__dirname);
20
+ expect(info.path).toBe(testDir);
22
21
  expect(info.totalFiles).toBeGreaterThanOrEqual(0);
23
22
  expect(info.totalLines).toBeGreaterThanOrEqual(0);
24
23
  expect(Array.isArray(info.primaryLanguages)).toBe(true);
@@ -26,7 +25,7 @@ describe('ProjectScanner', () => {
26
25
  });
27
26
 
28
27
  it('should detect TypeScript files', () => {
29
- const scanner = new ProjectScanner(__dirname, mockConfig);
28
+ const scanner = new ProjectScanner(testDir, mockConfig);
30
29
  const info = scanner.scan();
31
30
 
32
31
  if (info.totalFiles > 0) {
@@ -35,7 +34,7 @@ describe('ProjectScanner', () => {
35
34
  });
36
35
 
37
36
  it('should build a file tree structure', () => {
38
- const scanner = new ProjectScanner(__dirname, mockConfig);
37
+ const scanner = new ProjectScanner(testDir, mockConfig);
39
38
  const info = scanner.scan();
40
39
 
41
40
  expect(info.fileTree).toBeDefined();
@@ -46,7 +45,7 @@ describe('ProjectScanner', () => {
46
45
 
47
46
  describe('framework detection', () => {
48
47
  it('should detect frameworks from configuration files', () => {
49
- const scanner = new ProjectScanner(__dirname, mockConfig);
48
+ const scanner = new ProjectScanner(testDir, mockConfig);
50
49
  const info = scanner.scan();
51
50
 
52
51
  expect(Array.isArray(info.frameworks)).toBe(true);