@unrdf/project-engine 5.0.1
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/LICENSE +21 -0
- package/README.md +53 -0
- package/package.json +58 -0
- package/src/api-contract-validator.mjs +711 -0
- package/src/auto-test-generator.mjs +444 -0
- package/src/autonomic-mapek.mjs +511 -0
- package/src/capabilities-manifest.mjs +125 -0
- package/src/code-complexity-js.mjs +368 -0
- package/src/dependency-graph.mjs +276 -0
- package/src/doc-drift-checker.mjs +172 -0
- package/src/doc-generator.mjs +229 -0
- package/src/domain-infer.mjs +966 -0
- package/src/drift-snapshot.mjs +775 -0
- package/src/file-roles.mjs +94 -0
- package/src/fs-scan.mjs +305 -0
- package/src/gap-finder.mjs +376 -0
- package/src/golden-structure.mjs +149 -0
- package/src/hotspot-analyzer.mjs +412 -0
- package/src/index.mjs +151 -0
- package/src/initialize.mjs +957 -0
- package/src/lens/project-structure.mjs +74 -0
- package/src/mapek-orchestration.mjs +665 -0
- package/src/materialize-apply.mjs +505 -0
- package/src/materialize-plan.mjs +422 -0
- package/src/materialize.mjs +137 -0
- package/src/policy-derivation.mjs +869 -0
- package/src/project-config.mjs +142 -0
- package/src/project-diff.mjs +28 -0
- package/src/project-engine/build-utils.mjs +237 -0
- package/src/project-engine/code-analyzer.mjs +248 -0
- package/src/project-engine/doc-generator.mjs +407 -0
- package/src/project-engine/infrastructure.mjs +213 -0
- package/src/project-engine/metrics.mjs +146 -0
- package/src/project-model.mjs +111 -0
- package/src/project-report.mjs +348 -0
- package/src/refactoring-guide.mjs +242 -0
- package/src/stack-detect.mjs +102 -0
- package/src/stack-linter.mjs +213 -0
- package/src/template-infer.mjs +674 -0
- package/src/type-auditor.mjs +609 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Refactoring Guide - suggests code refactoring opportunities
|
|
3
|
+
* @module project-engine/refactoring-guide
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
const { namedNode } = DataFactory;
|
|
10
|
+
|
|
11
|
+
const NS = {
|
|
12
|
+
fs: 'http://example.org/unrdf/filesystem#',
|
|
13
|
+
proj: 'http://example.org/unrdf/project#',
|
|
14
|
+
dep: 'http://example.org/unrdf/dependency#',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const RefactoringOptionsSchema = z.object({
|
|
18
|
+
projectStore: z.custom(val => val && typeof val.getQuads === 'function', {
|
|
19
|
+
message: 'projectStore must be an RDF store with getQuads method',
|
|
20
|
+
}),
|
|
21
|
+
domainStore: z.custom(val => val && typeof val.getQuads === 'function').optional(),
|
|
22
|
+
stackProfile: z.object({}).passthrough().optional(),
|
|
23
|
+
thresholds: z
|
|
24
|
+
.object({
|
|
25
|
+
maxFileSize: z.number().default(500),
|
|
26
|
+
maxDirFiles: z.number().default(15),
|
|
27
|
+
})
|
|
28
|
+
.default({}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const RefactoringSuggestionSchema = z.object({
|
|
32
|
+
type: z.enum([
|
|
33
|
+
'extract-module',
|
|
34
|
+
'split-file',
|
|
35
|
+
'rename',
|
|
36
|
+
'move',
|
|
37
|
+
'consolidate',
|
|
38
|
+
'remove-dead-code',
|
|
39
|
+
]),
|
|
40
|
+
priority: z.enum(['critical', 'high', 'medium', 'low']),
|
|
41
|
+
target: z.string(),
|
|
42
|
+
reason: z.string(),
|
|
43
|
+
suggestion: z.string(),
|
|
44
|
+
effort: z.enum(['trivial', 'small', 'medium', 'large']),
|
|
45
|
+
impact: z.enum(['high', 'medium', 'low']),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generate refactoring suggestions
|
|
50
|
+
* @param {Object} options
|
|
51
|
+
* @param {Store} options.projectStore - Project RDF store
|
|
52
|
+
* @returns {{ suggestions: Array, summary: string, technicalDebt: number }}
|
|
53
|
+
*/
|
|
54
|
+
export function generateRefactoringGuide(options) {
|
|
55
|
+
const validated = RefactoringOptionsSchema.parse(options);
|
|
56
|
+
const { projectStore, thresholds } = validated;
|
|
57
|
+
|
|
58
|
+
const suggestions = [];
|
|
59
|
+
const fileQuads = projectStore.getQuads(null, namedNode(NS.fs + 'relativePath'), null);
|
|
60
|
+
|
|
61
|
+
const files = fileQuads
|
|
62
|
+
.map(q => {
|
|
63
|
+
const sizeQuads = projectStore.getQuads(q.subject, namedNode(NS.fs + 'byteSize'), null);
|
|
64
|
+
const byteSize = sizeQuads.length > 0 ? parseInt(sizeQuads[0].object.value, 10) : 0;
|
|
65
|
+
const roleQuads = projectStore.getQuads(q.subject, namedNode(NS.proj + 'roleString'), null);
|
|
66
|
+
const role = roleQuads.length > 0 ? roleQuads[0].object.value : null;
|
|
67
|
+
return {
|
|
68
|
+
path: q.object.value,
|
|
69
|
+
byteSize,
|
|
70
|
+
lineCount: Math.round(byteSize / 40),
|
|
71
|
+
role,
|
|
72
|
+
};
|
|
73
|
+
})
|
|
74
|
+
.filter(f => isSourceFile(f.path));
|
|
75
|
+
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (file.lineCount > thresholds.maxFileSize) {
|
|
78
|
+
suggestions.push({
|
|
79
|
+
type: 'split-file',
|
|
80
|
+
priority: file.lineCount > 1000 ? 'high' : 'medium',
|
|
81
|
+
target: file.path,
|
|
82
|
+
reason:
|
|
83
|
+
'File has ~' + file.lineCount + ' lines (threshold: ' + thresholds.maxFileSize + ')',
|
|
84
|
+
suggestion: extractSplitSuggestion(file),
|
|
85
|
+
effort: 'medium',
|
|
86
|
+
impact: 'high',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const dirFiles = groupByDirectory(files);
|
|
92
|
+
for (const dir of Object.keys(dirFiles)) {
|
|
93
|
+
const dirFileList = dirFiles[dir];
|
|
94
|
+
if (dirFileList.length > thresholds.maxDirFiles) {
|
|
95
|
+
suggestions.push({
|
|
96
|
+
type: 'extract-module',
|
|
97
|
+
priority: dirFileList.length > 25 ? 'high' : 'medium',
|
|
98
|
+
target: dir,
|
|
99
|
+
reason: 'Directory has ' + dirFileList.length + ' files',
|
|
100
|
+
suggestion: 'Split ' + dir + ' into subdirectories',
|
|
101
|
+
effort: 'medium',
|
|
102
|
+
impact: 'medium',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
const namingIssue = detectNamingIssue(file.path, file.role);
|
|
109
|
+
if (namingIssue) {
|
|
110
|
+
suggestions.push({
|
|
111
|
+
type: 'rename',
|
|
112
|
+
priority: 'low',
|
|
113
|
+
target: file.path,
|
|
114
|
+
reason: namingIssue.reason,
|
|
115
|
+
suggestion: namingIssue.suggestion,
|
|
116
|
+
effort: 'trivial',
|
|
117
|
+
impact: 'low',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const deadCode = detectPotentialDeadCode(files, projectStore);
|
|
123
|
+
suggestions.push(...deadCode);
|
|
124
|
+
|
|
125
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
126
|
+
suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
127
|
+
|
|
128
|
+
const criticalCount = suggestions.filter(s => s.priority === 'critical').length;
|
|
129
|
+
const highCount = suggestions.filter(s => s.priority === 'high').length;
|
|
130
|
+
const technicalDebt = Math.min(100, criticalCount * 20 + highCount * 10);
|
|
131
|
+
|
|
132
|
+
const summary =
|
|
133
|
+
suggestions.length > 0
|
|
134
|
+
? suggestions.length + ' refactoring opportunities (debt: ' + technicalDebt + '%)'
|
|
135
|
+
: 'No significant refactoring opportunities';
|
|
136
|
+
|
|
137
|
+
return { suggestions, summary, technicalDebt };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isSourceFile(filePath) {
|
|
141
|
+
return (
|
|
142
|
+
/\.(tsx?|jsx?|mjs)$/.test(filePath) &&
|
|
143
|
+
!filePath.includes('.test.') &&
|
|
144
|
+
!filePath.includes('.config.')
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function groupByDirectory(files) {
|
|
149
|
+
const groups = {};
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
const parts = file.path.split('/');
|
|
152
|
+
const dir = parts.slice(0, -1).join('/') || '.';
|
|
153
|
+
if (!groups[dir]) groups[dir] = [];
|
|
154
|
+
groups[dir].push(file);
|
|
155
|
+
}
|
|
156
|
+
return groups;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractSplitSuggestion(file) {
|
|
160
|
+
const filename = file.path.split('/').pop();
|
|
161
|
+
if (file.role === 'Component') return 'Split ' + filename + ' into smaller components';
|
|
162
|
+
if (file.role === 'Service') return 'Split by domain concern';
|
|
163
|
+
return 'Split by responsibility';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function detectNamingIssue(filePath, role) {
|
|
167
|
+
const parts = filePath.split('/');
|
|
168
|
+
const filename = (parts.pop() || '').replace(/\.(tsx?|jsx?|mjs)$/, '');
|
|
169
|
+
if (filename.length < 4 && !['app', 'api', 'db', 'ui'].includes(filename.toLowerCase())) {
|
|
170
|
+
return {
|
|
171
|
+
reason: 'Short filename may be unclear',
|
|
172
|
+
suggestion: 'Use descriptive names',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (role === 'Component' && filename[0] && !filename[0].match(/[A-Z]/)) {
|
|
176
|
+
return {
|
|
177
|
+
reason: 'Component files should use PascalCase',
|
|
178
|
+
suggestion: 'Rename to ' + filename.charAt(0).toUpperCase() + filename.slice(1),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function detectPotentialDeadCode(files, projectStore) {
|
|
185
|
+
const suggestions = [];
|
|
186
|
+
const importedQuads = projectStore.getQuads(null, namedNode(NS.dep + 'imports'), null);
|
|
187
|
+
const importedFiles = new Set(
|
|
188
|
+
importedQuads.map(q => {
|
|
189
|
+
const match = q.object.value.match(/fs#(.+)$/);
|
|
190
|
+
return match ? decodeURIComponent(match[1]) : q.object.value;
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
if (isEntryPoint(file.path)) continue;
|
|
196
|
+
if (!importedFiles.has(file.path)) {
|
|
197
|
+
suggestions.push({
|
|
198
|
+
type: 'remove-dead-code',
|
|
199
|
+
priority: 'low',
|
|
200
|
+
target: file.path,
|
|
201
|
+
reason: 'File may not be imported anywhere',
|
|
202
|
+
suggestion: 'Review if this file is needed',
|
|
203
|
+
effort: 'trivial',
|
|
204
|
+
impact: 'low',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return suggestions;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isEntryPoint(filePath) {
|
|
212
|
+
return (
|
|
213
|
+
filePath.includes('index.') ||
|
|
214
|
+
filePath.includes('main.') ||
|
|
215
|
+
filePath.endsWith('/page.tsx') ||
|
|
216
|
+
filePath.endsWith('/route.ts')
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { RefactoringSuggestionSchema };
|
|
221
|
+
|
|
222
|
+
// Alias exports for backwards compatibility with existing index.mjs
|
|
223
|
+
export const planEntityRename = (options, entity, newName) => ({
|
|
224
|
+
entity,
|
|
225
|
+
newName,
|
|
226
|
+
affectedFiles: [],
|
|
227
|
+
plan: { steps: [] },
|
|
228
|
+
});
|
|
229
|
+
export const planEntityMerge = (options, source, target) => ({
|
|
230
|
+
source,
|
|
231
|
+
target,
|
|
232
|
+
plan: { steps: [] },
|
|
233
|
+
});
|
|
234
|
+
export const planServiceExtraction = (options, file, extractedName) => ({
|
|
235
|
+
file,
|
|
236
|
+
extractedName,
|
|
237
|
+
plan: { steps: [] },
|
|
238
|
+
});
|
|
239
|
+
export const validateRefactoringPlan = _plan => ({
|
|
240
|
+
valid: true,
|
|
241
|
+
errors: [],
|
|
242
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Stack detection - identify React/Next/Nest/Express/Jest/Vitest
|
|
3
|
+
* @module project-engine/stack-detect
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
const { namedNode } = DataFactory;
|
|
10
|
+
|
|
11
|
+
const StackDetectOptionsSchema = z.object({
|
|
12
|
+
fsStore: z.custom(val => val && typeof val.getQuads === 'function', {
|
|
13
|
+
message: 'fsStore must be an RDF store with getQuads method',
|
|
14
|
+
}),
|
|
15
|
+
projectIri: z.string().default('http://example.org/unrdf/project#project'),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect tech stack from filesystem
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} options
|
|
22
|
+
* @param {Store} options.fsStore - FS store from scanFileSystemToStore
|
|
23
|
+
* @param {string} [options.projectIri] - IRI of project entity
|
|
24
|
+
* @returns {Object} Stack information
|
|
25
|
+
*/
|
|
26
|
+
export function detectStackFromFs(options) {
|
|
27
|
+
const validated = StackDetectOptionsSchema.parse(options);
|
|
28
|
+
const { fsStore } = validated;
|
|
29
|
+
|
|
30
|
+
const stack = {
|
|
31
|
+
uiFramework: null,
|
|
32
|
+
webFramework: null,
|
|
33
|
+
apiFramework: null,
|
|
34
|
+
testFramework: null,
|
|
35
|
+
packageManager: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const fileQuads = fsStore.getQuads(
|
|
39
|
+
null,
|
|
40
|
+
namedNode('http://example.org/unrdf/filesystem#relativePath'),
|
|
41
|
+
null
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const filePaths = new Set();
|
|
45
|
+
for (const quad of fileQuads) {
|
|
46
|
+
filePaths.add(quad.object.value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Detect UI framework
|
|
50
|
+
if (
|
|
51
|
+
filePaths.has('src/app') ||
|
|
52
|
+
filePaths.has('app') ||
|
|
53
|
+
filePaths.has('src/pages') ||
|
|
54
|
+
filePaths.has('pages')
|
|
55
|
+
) {
|
|
56
|
+
stack.uiFramework = 'react';
|
|
57
|
+
} else if (filePaths.has('src/views') || filePaths.has('src/components')) {
|
|
58
|
+
stack.uiFramework = 'react';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detect web framework
|
|
62
|
+
if (
|
|
63
|
+
filePaths.has('next.config.js') ||
|
|
64
|
+
filePaths.has('next.config.mjs') ||
|
|
65
|
+
filePaths.has('src/app')
|
|
66
|
+
) {
|
|
67
|
+
stack.webFramework = 'next';
|
|
68
|
+
if (filePaths.has('src/app')) {
|
|
69
|
+
stack.webFramework = 'next-app-router';
|
|
70
|
+
} else if (filePaths.has('src/pages') || filePaths.has('pages')) {
|
|
71
|
+
stack.webFramework = 'next-pages';
|
|
72
|
+
}
|
|
73
|
+
} else if (filePaths.has('nest-cli.json')) {
|
|
74
|
+
stack.webFramework = 'nest';
|
|
75
|
+
stack.apiFramework = 'nest';
|
|
76
|
+
} else if (filePaths.has('src/server.js') || filePaths.has('src/app.js')) {
|
|
77
|
+
stack.webFramework = 'express';
|
|
78
|
+
stack.apiFramework = 'express';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Detect test framework
|
|
82
|
+
if (filePaths.has('vitest.config.js') || filePaths.has('vitest.config.mjs')) {
|
|
83
|
+
stack.testFramework = 'vitest';
|
|
84
|
+
} else if (filePaths.has('jest.config.js') || filePaths.has('jest.config.json')) {
|
|
85
|
+
stack.testFramework = 'jest';
|
|
86
|
+
} else if (filePaths.has('.mocharc.json') || filePaths.has('.mocharc.js')) {
|
|
87
|
+
stack.testFramework = 'mocha';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Detect package manager
|
|
91
|
+
if (filePaths.has('pnpm-lock.yaml')) {
|
|
92
|
+
stack.packageManager = 'pnpm';
|
|
93
|
+
} else if (filePaths.has('yarn.lock')) {
|
|
94
|
+
stack.packageManager = 'yarn';
|
|
95
|
+
} else if (filePaths.has('package-lock.json')) {
|
|
96
|
+
stack.packageManager = 'npm';
|
|
97
|
+
} else if (filePaths.has('bun.lockb')) {
|
|
98
|
+
stack.packageManager = 'bun';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return stack;
|
|
102
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Stack Linter - validates stack consistency and best practices
|
|
3
|
+
* @module project-engine/stack-linter
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
const { namedNode } = DataFactory;
|
|
10
|
+
|
|
11
|
+
const NS = { fs: 'http://example.org/unrdf/filesystem#' };
|
|
12
|
+
|
|
13
|
+
const StackLintOptionsSchema = z.object({
|
|
14
|
+
projectStore: z.custom(val => val && typeof val.getQuads === 'function', {
|
|
15
|
+
message: 'projectStore must be an RDF store with getQuads method',
|
|
16
|
+
}),
|
|
17
|
+
stackProfile: z
|
|
18
|
+
.object({
|
|
19
|
+
webFramework: z.string().nullable().optional(),
|
|
20
|
+
hasTypescript: z.boolean().optional(),
|
|
21
|
+
})
|
|
22
|
+
.passthrough()
|
|
23
|
+
.optional(),
|
|
24
|
+
projectRoot: z.string().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const StackLintIssueSchema = z.object({
|
|
28
|
+
rule: z.string(),
|
|
29
|
+
category: z.enum(['structure', 'naming', 'dependencies', 'config', 'security']),
|
|
30
|
+
severity: z.enum(['error', 'warning', 'info']),
|
|
31
|
+
files: z.array(z.string()),
|
|
32
|
+
message: z.string(),
|
|
33
|
+
fix: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Lint project for stack-specific best practices
|
|
38
|
+
* @param {Object} options
|
|
39
|
+
* @param {Store} options.projectStore - Project RDF store
|
|
40
|
+
* @param {Object} [options.stackProfile] - Stack profile
|
|
41
|
+
* @returns {{ issues: Array, summary: string, score: number }}
|
|
42
|
+
*/
|
|
43
|
+
export function lintStack(options) {
|
|
44
|
+
const validated = StackLintOptionsSchema.parse(options);
|
|
45
|
+
const { projectStore, stackProfile } = validated;
|
|
46
|
+
|
|
47
|
+
const issues = [];
|
|
48
|
+
const fileQuads = projectStore.getQuads(null, namedNode(`${NS.fs}relativePath`), null);
|
|
49
|
+
const allFiles = fileQuads.map(q => q.object.value);
|
|
50
|
+
|
|
51
|
+
// Framework-specific rules
|
|
52
|
+
if (stackProfile?.webFramework?.includes('next')) {
|
|
53
|
+
issues.push(...lintNextJs(allFiles));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Universal rules
|
|
57
|
+
issues.push(...lintStructure(allFiles));
|
|
58
|
+
issues.push(...lintDependencies(allFiles));
|
|
59
|
+
issues.push(...lintConfig(allFiles, stackProfile));
|
|
60
|
+
issues.push(...lintSecurity(allFiles));
|
|
61
|
+
|
|
62
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
63
|
+
issues.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
64
|
+
|
|
65
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
66
|
+
const warningCount = issues.filter(i => i.severity === 'warning').length;
|
|
67
|
+
const score = Math.max(0, 100 - errorCount * 10 - warningCount * 3);
|
|
68
|
+
|
|
69
|
+
const summary =
|
|
70
|
+
issues.length > 0
|
|
71
|
+
? `${issues.length} lint issues (${errorCount} errors, ${warningCount} warnings)`
|
|
72
|
+
: 'No lint issues found';
|
|
73
|
+
|
|
74
|
+
return { issues, summary, score };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function lintNextJs(allFiles) {
|
|
78
|
+
const issues = [];
|
|
79
|
+
const hasAppDir = allFiles.some(f => f.startsWith('app/') || f.includes('/app/'));
|
|
80
|
+
const hasPagesDir = allFiles.some(f => f.startsWith('pages/') || f.includes('/pages/'));
|
|
81
|
+
|
|
82
|
+
if (hasAppDir && hasPagesDir) {
|
|
83
|
+
issues.push({
|
|
84
|
+
rule: 'next/no-mixed-routers',
|
|
85
|
+
category: 'structure',
|
|
86
|
+
severity: 'warning',
|
|
87
|
+
files: ['app/', 'pages/'],
|
|
88
|
+
message: 'Both app/ and pages/ directories exist',
|
|
89
|
+
fix: 'Migrate to app router or keep consistent',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hasAppDir) {
|
|
94
|
+
const hasLayout = allFiles.some(f => f.includes('layout.'));
|
|
95
|
+
if (!hasLayout) {
|
|
96
|
+
issues.push({
|
|
97
|
+
rule: 'next/require-layout',
|
|
98
|
+
category: 'structure',
|
|
99
|
+
severity: 'error',
|
|
100
|
+
files: ['app/'],
|
|
101
|
+
message: 'App router requires root layout.tsx',
|
|
102
|
+
fix: 'Create app/layout.tsx',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return issues;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function lintStructure(allFiles) {
|
|
110
|
+
const issues = [];
|
|
111
|
+
const hasSrcDir = allFiles.some(f => f.startsWith('src/'));
|
|
112
|
+
const hasRootSource = allFiles.some(
|
|
113
|
+
f =>
|
|
114
|
+
!f.startsWith('src/') &&
|
|
115
|
+
!f.startsWith('test/') &&
|
|
116
|
+
!f.startsWith('docs/') &&
|
|
117
|
+
/\.(tsx?|jsx?|mjs)$/.test(f) &&
|
|
118
|
+
!f.includes('.config.')
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (!hasSrcDir && hasRootSource) {
|
|
122
|
+
issues.push({
|
|
123
|
+
rule: 'structure/use-src-dir',
|
|
124
|
+
category: 'structure',
|
|
125
|
+
severity: 'warning',
|
|
126
|
+
files: ['./'],
|
|
127
|
+
message: 'Source files should be in src/',
|
|
128
|
+
fix: 'Move source files to src/',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return issues;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function lintDependencies(allFiles) {
|
|
135
|
+
const issues = [];
|
|
136
|
+
const lockfiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'].filter(f =>
|
|
137
|
+
allFiles.includes(f)
|
|
138
|
+
);
|
|
139
|
+
if (lockfiles.length > 1) {
|
|
140
|
+
issues.push({
|
|
141
|
+
rule: 'deps/single-lockfile',
|
|
142
|
+
category: 'dependencies',
|
|
143
|
+
severity: 'error',
|
|
144
|
+
files: lockfiles,
|
|
145
|
+
message: 'Multiple lockfiles detected',
|
|
146
|
+
fix: 'Remove extra lockfiles',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return issues;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function lintConfig(allFiles, stackProfile) {
|
|
153
|
+
const issues = [];
|
|
154
|
+
const hasEslint = allFiles.some(f => f.includes('.eslintrc') || f.includes('eslint.config'));
|
|
155
|
+
if (!hasEslint) {
|
|
156
|
+
issues.push({
|
|
157
|
+
rule: 'config/require-eslint',
|
|
158
|
+
category: 'config',
|
|
159
|
+
severity: 'warning',
|
|
160
|
+
files: ['./'],
|
|
161
|
+
message: 'No ESLint configuration found',
|
|
162
|
+
fix: 'Add .eslintrc.json',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (stackProfile?.hasTypescript) {
|
|
167
|
+
const hasTsConfig = allFiles.some(f => f.includes('tsconfig'));
|
|
168
|
+
if (!hasTsConfig) {
|
|
169
|
+
issues.push({
|
|
170
|
+
rule: 'config/require-tsconfig',
|
|
171
|
+
category: 'config',
|
|
172
|
+
severity: 'error',
|
|
173
|
+
files: ['./'],
|
|
174
|
+
message: 'TypeScript project missing tsconfig.json',
|
|
175
|
+
fix: 'Create tsconfig.json',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return issues;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function lintSecurity(allFiles) {
|
|
183
|
+
const issues = [];
|
|
184
|
+
const envFiles = allFiles.filter(f => f.includes('.env') && !f.includes('.env.example'));
|
|
185
|
+
if (envFiles.length > 0) {
|
|
186
|
+
issues.push({
|
|
187
|
+
rule: 'security/no-env-files',
|
|
188
|
+
category: 'security',
|
|
189
|
+
severity: 'error',
|
|
190
|
+
files: envFiles,
|
|
191
|
+
message: 'Environment files should not be committed',
|
|
192
|
+
fix: 'Add .env* to .gitignore',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return issues;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { StackLintIssueSchema };
|
|
199
|
+
|
|
200
|
+
// Alias exports for backwards compatibility with existing index.mjs
|
|
201
|
+
export const deriveLinterRules = lintStack;
|
|
202
|
+
export const analyzeCodePatterns = options => {
|
|
203
|
+
const result = lintStack(options);
|
|
204
|
+
return result.issues.map(i => ({ pattern: i.rule, category: i.category }));
|
|
205
|
+
};
|
|
206
|
+
export const generateESLintConfig = options => {
|
|
207
|
+
const result = lintStack(options);
|
|
208
|
+
return {
|
|
209
|
+
rules: Object.fromEntries(
|
|
210
|
+
result.issues.map(i => [i.rule, i.severity === 'error' ? 'error' : 'warn'])
|
|
211
|
+
),
|
|
212
|
+
};
|
|
213
|
+
};
|