@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.
- package/dist/src/core/analyzer.d.ts +42 -0
- package/dist/src/core/analyzer.js +431 -0
- package/dist/src/core/analyzer.js.map +1 -0
- package/dist/src/core/analyzers/forecast.d.ts +84 -0
- package/dist/src/core/analyzers/forecast.js +338 -0
- package/dist/src/core/analyzers/forecast.js.map +1 -0
- package/dist/src/core/analyzers/index.d.ts +9 -0
- package/dist/src/core/analyzers/index.js +7 -0
- package/dist/src/core/analyzers/index.js.map +1 -0
- package/dist/src/core/analyzers/temporal-scorer.d.ts +71 -0
- package/dist/src/core/analyzers/temporal-scorer.js +141 -0
- package/dist/src/core/analyzers/temporal-scorer.js.map +1 -0
- package/dist/src/core/anti-patterns.d.ts +28 -0
- package/dist/src/core/anti-patterns.js +264 -0
- package/dist/src/core/anti-patterns.js.map +1 -0
- package/dist/src/core/ast/ast-parser.interface.d.ts +20 -0
- package/dist/src/core/ast/ast-parser.interface.js +2 -0
- package/dist/src/core/ast/ast-parser.interface.js.map +1 -0
- package/dist/src/core/ast/path-resolver.d.ts +13 -0
- package/dist/src/core/ast/path-resolver.js +54 -0
- package/dist/src/core/ast/path-resolver.js.map +1 -0
- package/dist/src/core/ast/tree-sitter-parser.d.ts +10 -0
- package/dist/src/core/ast/tree-sitter-parser.js +142 -0
- package/dist/src/core/ast/tree-sitter-parser.js.map +1 -0
- package/dist/src/core/config.d.ts +11 -0
- package/dist/src/core/config.js +112 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/diagram.d.ts +9 -0
- package/dist/src/core/diagram.js +101 -0
- package/dist/src/core/diagram.js.map +1 -0
- package/dist/src/core/i18n.d.ts +14 -0
- package/dist/src/core/i18n.js +54 -0
- package/dist/src/core/i18n.js.map +1 -0
- package/dist/src/core/locales/en.d.ts +2 -0
- package/dist/src/core/locales/en.js +337 -0
- package/dist/src/core/locales/en.js.map +1 -0
- package/dist/src/core/locales/pt-BR.d.ts +172 -0
- package/dist/src/core/locales/pt-BR.js +337 -0
- package/dist/src/core/locales/pt-BR.js.map +1 -0
- package/dist/src/core/locales/types.d.ts +86 -0
- package/dist/src/core/locales/types.js +2 -0
- package/dist/src/core/locales/types.js.map +1 -0
- package/dist/src/core/plugin-loader.d.ts +11 -0
- package/dist/src/core/plugin-loader.js +67 -0
- package/dist/src/core/plugin-loader.js.map +1 -0
- package/dist/src/core/project-summarizer.d.ts +16 -0
- package/dist/src/core/project-summarizer.js +37 -0
- package/dist/src/core/project-summarizer.js.map +1 -0
- package/dist/src/core/refactor-engine.d.ts +18 -0
- package/dist/src/core/refactor-engine.js +87 -0
- package/dist/src/core/refactor-engine.js.map +1 -0
- package/dist/src/core/rules/barrel-optimizer.d.ts +13 -0
- package/dist/src/core/rules/barrel-optimizer.js +76 -0
- package/dist/src/core/rules/barrel-optimizer.js.map +1 -0
- package/dist/src/core/rules/dead-code-detector.d.ts +21 -0
- package/dist/src/core/rules/dead-code-detector.js +116 -0
- package/dist/src/core/rules/dead-code-detector.js.map +1 -0
- package/dist/src/core/rules/hub-splitter.d.ts +13 -0
- package/dist/src/core/rules/hub-splitter.js +117 -0
- package/dist/src/core/rules/hub-splitter.js.map +1 -0
- package/dist/src/core/rules/import-organizer.d.ts +13 -0
- package/dist/src/core/rules/import-organizer.js +84 -0
- package/dist/src/core/rules/import-organizer.js.map +1 -0
- package/dist/src/core/rules/module-grouper.d.ts +13 -0
- package/dist/src/core/rules/module-grouper.js +116 -0
- package/dist/src/core/rules/module-grouper.js.map +1 -0
- package/dist/src/core/rules-engine.d.ts +7 -0
- package/dist/src/core/rules-engine.js +89 -0
- package/dist/src/core/rules-engine.js.map +1 -0
- package/dist/src/core/scorer.d.ts +15 -0
- package/dist/src/core/scorer.js +165 -0
- package/dist/src/core/scorer.js.map +1 -0
- package/dist/src/core/summarizer/keyword-extractor.d.ts +6 -0
- package/dist/src/core/summarizer/keyword-extractor.js +38 -0
- package/dist/src/core/summarizer/keyword-extractor.js.map +1 -0
- package/dist/src/core/summarizer/module-inferrer.d.ts +11 -0
- package/dist/src/core/summarizer/module-inferrer.js +171 -0
- package/dist/src/core/summarizer/module-inferrer.js.map +1 -0
- package/dist/src/core/summarizer/package-reader.d.ts +3 -0
- package/dist/src/core/summarizer/package-reader.js +33 -0
- package/dist/src/core/summarizer/package-reader.js.map +1 -0
- package/dist/src/core/summarizer/purpose-inferrer.d.ts +8 -0
- package/dist/src/core/summarizer/purpose-inferrer.js +179 -0
- package/dist/src/core/summarizer/purpose-inferrer.js.map +1 -0
- package/dist/src/core/summarizer/readme-reader.d.ts +3 -0
- package/dist/src/core/summarizer/readme-reader.js +24 -0
- package/dist/src/core/summarizer/readme-reader.js.map +1 -0
- package/dist/src/core/types/architect-rules.d.ts +27 -0
- package/dist/src/core/types/architect-rules.js +2 -0
- package/dist/src/core/types/architect-rules.js.map +1 -0
- package/dist/src/core/types/core.d.ts +87 -0
- package/dist/src/core/types/core.js +2 -0
- package/dist/src/core/types/core.js.map +1 -0
- package/dist/src/core/types/infrastructure.d.ts +38 -0
- package/dist/src/core/types/infrastructure.js +2 -0
- package/dist/src/core/types/infrastructure.js.map +1 -0
- package/dist/src/core/types/plugin.d.ts +12 -0
- package/dist/src/core/types/plugin.js +2 -0
- package/dist/src/core/types/plugin.js.map +1 -0
- package/dist/src/core/types/rules.d.ts +53 -0
- package/dist/src/core/types/rules.js +2 -0
- package/dist/src/core/types/rules.js.map +1 -0
- package/dist/src/core/types/summarizer.d.ts +12 -0
- package/dist/src/core/types/summarizer.js +2 -0
- package/dist/src/core/types/summarizer.js.map +1 -0
- package/dist/src/infrastructure/git-cache.d.ts +6 -0
- package/dist/src/infrastructure/git-cache.js +41 -0
- package/dist/src/infrastructure/git-cache.js.map +1 -0
- package/dist/src/infrastructure/git-history.d.ts +112 -0
- package/dist/src/infrastructure/git-history.js +340 -0
- package/dist/src/infrastructure/git-history.js.map +1 -0
- package/dist/src/infrastructure/logger.d.ts +20 -0
- package/dist/src/infrastructure/logger.js +57 -0
- package/dist/src/infrastructure/logger.js.map +1 -0
- package/dist/src/infrastructure/scanner.d.ts +31 -0
- package/dist/src/infrastructure/scanner.js +334 -0
- package/dist/src/infrastructure/scanner.js.map +1 -0
- package/dist/tests/analyzers-integration.test.d.ts +7 -0
- package/dist/tests/analyzers-integration.test.js +140 -0
- package/dist/tests/analyzers-integration.test.js.map +1 -0
- package/dist/tests/anti-patterns.test.d.ts +1 -0
- package/dist/tests/anti-patterns.test.js +81 -0
- package/dist/tests/anti-patterns.test.js.map +1 -0
- package/dist/tests/ast-parser.test.d.ts +1 -0
- package/dist/tests/ast-parser.test.js +94 -0
- package/dist/tests/ast-parser.test.js.map +1 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.d.ts +1 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.js +9 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.js.map +1 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.d.ts +2 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.js +11 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.js.map +1 -0
- package/dist/tests/forecast.test.d.ts +7 -0
- package/dist/tests/forecast.test.js +380 -0
- package/dist/tests/forecast.test.js.map +1 -0
- package/dist/tests/git-history.test.d.ts +7 -0
- package/dist/tests/git-history.test.js +193 -0
- package/dist/tests/git-history.test.js.map +1 -0
- package/dist/tests/i18n.test.d.ts +1 -0
- package/dist/tests/i18n.test.js +39 -0
- package/dist/tests/i18n.test.js.map +1 -0
- package/dist/tests/monorepo-scan.test.d.ts +11 -0
- package/dist/tests/monorepo-scan.test.js +143 -0
- package/dist/tests/monorepo-scan.test.js.map +1 -0
- package/dist/tests/plugin-loader.test.d.ts +1 -0
- package/dist/tests/plugin-loader.test.js +31 -0
- package/dist/tests/plugin-loader.test.js.map +1 -0
- package/dist/tests/rules-engine.test.d.ts +1 -0
- package/dist/tests/rules-engine.test.js +112 -0
- package/dist/tests/rules-engine.test.js.map +1 -0
- package/dist/tests/scanner.test.d.ts +1 -0
- package/dist/tests/scanner.test.js +44 -0
- package/dist/tests/scanner.test.js.map +1 -0
- package/dist/tests/scorer.test.d.ts +1 -0
- package/dist/tests/scorer.test.js +610 -0
- package/dist/tests/scorer.test.js.map +1 -0
- package/dist/tests/temporal-scorer.test.d.ts +7 -0
- package/dist/tests/temporal-scorer.test.js +239 -0
- package/dist/tests/temporal-scorer.test.js.map +1 -0
- package/package.json +29 -0
- package/src/core/analyzer.ts +499 -0
- package/src/core/analyzers/forecast.ts +497 -0
- package/src/core/analyzers/index.ts +33 -0
- package/src/core/analyzers/temporal-scorer.ts +227 -0
- package/src/core/anti-patterns.ts +324 -0
- package/src/core/ast/ast-parser.interface.ts +21 -0
- package/src/core/ast/path-resolver.ts +61 -0
- package/src/core/ast/tree-sitter-parser.ts +158 -0
- package/src/core/config.ts +125 -0
- package/src/core/diagram.ts +129 -0
- package/src/core/i18n.ts +64 -0
- package/src/core/locales/en.ts +340 -0
- package/src/core/locales/pt-BR.ts +341 -0
- package/src/core/locales/types.ts +95 -0
- package/src/core/plugin-loader.ts +80 -0
- package/src/core/project-summarizer.ts +42 -0
- package/src/core/refactor-engine.ts +112 -0
- package/src/core/rules/barrel-optimizer.ts +99 -0
- package/src/core/rules/dead-code-detector.ts +134 -0
- package/src/core/rules/hub-splitter.ts +135 -0
- package/src/core/rules/import-organizer.ts +100 -0
- package/src/core/rules/module-grouper.ts +133 -0
- package/src/core/rules-engine.ts +100 -0
- package/src/core/scorer.ts +181 -0
- package/src/core/summarizer/keyword-extractor.ts +53 -0
- package/src/core/summarizer/module-inferrer.ts +194 -0
- package/src/core/summarizer/package-reader.ts +34 -0
- package/src/core/summarizer/purpose-inferrer.ts +197 -0
- package/src/core/summarizer/readme-reader.ts +24 -0
- package/src/core/types/architect-rules.ts +29 -0
- package/src/core/types/core.ts +94 -0
- package/src/core/types/infrastructure.ts +41 -0
- package/src/core/types/plugin.ts +19 -0
- package/src/core/types/rules.ts +51 -0
- package/src/core/types/summarizer.ts +8 -0
- package/src/infrastructure/git-cache.ts +52 -0
- package/src/infrastructure/git-history.ts +496 -0
- package/src/infrastructure/logger.ts +68 -0
- package/src/infrastructure/scanner.ts +349 -0
- package/tests/analyzers-integration.test.ts +174 -0
- package/tests/anti-patterns.test.ts +95 -0
- package/tests/ast-parser.test.ts +102 -0
- package/tests/fixtures/monorepo/package.json +6 -0
- package/tests/fixtures/monorepo/packages/app/package.json +12 -0
- package/tests/fixtures/monorepo/packages/app/src/index.ts +6 -0
- package/tests/fixtures/monorepo/packages/core/package.json +7 -0
- package/tests/fixtures/monorepo/packages/core/src/index.ts +7 -0
- package/tests/forecast.test.ts +504 -0
- package/tests/git-history.test.ts +254 -0
- package/tests/i18n.test.ts +47 -0
- package/tests/monorepo-scan.test.ts +170 -0
- package/tests/plugin-loader.test.ts +40 -0
- package/tests/rules-engine.test.ts +131 -0
- package/tests/scanner.test.ts +54 -0
- package/tests/scorer.test.ts +675 -0
- package/tests/temporal-scorer.test.ts +306 -0
- 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
|
+
});
|