agent-workflow-kit-cli 1.2.0 → 1.3.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/cli/commands/add.js +50 -12
- package/dist/cli/commands/adr.js +98 -0
- package/dist/cli/commands/export.js +13 -0
- package/dist/cli/commands/init.js +136 -30
- package/dist/cli/commands/profile.js +35 -0
- package/dist/cli/commands/role.js +75 -0
- package/dist/cli/commands/run.js +78 -0
- package/dist/cli/commands/workflow.js +95 -0
- package/dist/cli/index.js +193 -2
- package/dist/core/awos/adr.js +208 -0
- package/dist/core/awos/intelligence.js +235 -0
- package/dist/core/awos/profiles.js +272 -0
- package/dist/core/awos/registry.js +224 -0
- package/dist/core/awos/runtime.js +322 -0
- package/dist/core/awos/types.js +5 -0
- package/dist/core/config.js +27 -0
- package/dist/core/parser.js +143 -0
- package/dist/core/renderer.js +74 -23
- package/package.json +3 -2
- package/templates/common/ide-rules.hbs +12 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import { detectProjectModules } from "../detector.js";
|
|
9
|
+
import { parseImports } from "../parser.js";
|
|
10
|
+
import { loadAWOSPlugins } from "./registry.js";
|
|
11
|
+
const CACHE_DIR = ".agents/cache";
|
|
12
|
+
const CACHE_FILE = path.join(CACHE_DIR, "repo_context.json");
|
|
13
|
+
/**
|
|
14
|
+
* Calculates a composite MD5/SHA256 hash of all configuration manifest files in the repository.
|
|
15
|
+
*/
|
|
16
|
+
export async function calculateManifestHash(workspaceRoot) {
|
|
17
|
+
const manifests = [
|
|
18
|
+
"package.json",
|
|
19
|
+
"package-lock.json",
|
|
20
|
+
"pom.xml",
|
|
21
|
+
"build.gradle",
|
|
22
|
+
"build.gradle.kts",
|
|
23
|
+
"pyproject.toml",
|
|
24
|
+
"requirements.txt",
|
|
25
|
+
"Pipfile",
|
|
26
|
+
"tsconfig.json",
|
|
27
|
+
];
|
|
28
|
+
const hash = crypto.createHash("sha256");
|
|
29
|
+
let foundAny = false;
|
|
30
|
+
for (const file of manifests) {
|
|
31
|
+
const fullPath = path.join(workspaceRoot, file);
|
|
32
|
+
try {
|
|
33
|
+
const stat = await fs.stat(fullPath);
|
|
34
|
+
if (stat.isFile()) {
|
|
35
|
+
const content = await fs.readFile(fullPath);
|
|
36
|
+
hash.update(file);
|
|
37
|
+
hash.update(content);
|
|
38
|
+
foundAny = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Ignore missing manifest files
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Also include directory structure hash of sub-modules
|
|
46
|
+
try {
|
|
47
|
+
const submodules = await detectProjectModules(workspaceRoot);
|
|
48
|
+
for (const sub of submodules) {
|
|
49
|
+
hash.update(sub.name);
|
|
50
|
+
hash.update(sub.stacks.join(","));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Ignore errors
|
|
55
|
+
}
|
|
56
|
+
return foundAny ? hash.digest("hex") : "no-manifests-hash";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parses files in a directory to identify imports and build a dependency relationship between directories.
|
|
60
|
+
*/
|
|
61
|
+
async function analyzeImportGraph(workspaceRoot, modules) {
|
|
62
|
+
const dependencies = {};
|
|
63
|
+
for (const mod of modules) {
|
|
64
|
+
dependencies[mod] = new Set();
|
|
65
|
+
}
|
|
66
|
+
// Helper to find files recursively
|
|
67
|
+
async function scanDir(dir, currentModule) {
|
|
68
|
+
try {
|
|
69
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const fullPath = path.join(dir, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
if (entry.name.startsWith(".") ||
|
|
74
|
+
entry.name === "node_modules" ||
|
|
75
|
+
entry.name === "dist" ||
|
|
76
|
+
entry.name === "build" ||
|
|
77
|
+
entry.name === "target") {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
await scanDir(fullPath, currentModule);
|
|
81
|
+
}
|
|
82
|
+
else if (entry.isFile() && /\.(ts|tsx|java|py)$/.test(entry.name)) {
|
|
83
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
84
|
+
const parsedImports = await parseImports(fullPath, content);
|
|
85
|
+
for (const importedRef of parsedImports) {
|
|
86
|
+
for (const mod of modules) {
|
|
87
|
+
if (mod !== currentModule &&
|
|
88
|
+
(importedRef.includes(`/${mod}`) || importedRef.includes(`.${mod}`))) {
|
|
89
|
+
dependencies[currentModule].add(mod);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Ignore read errors
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const mod of modules) {
|
|
101
|
+
const modulePath = path.join(workspaceRoot, mod);
|
|
102
|
+
await scanDir(modulePath, mod);
|
|
103
|
+
}
|
|
104
|
+
// Convert Sets to arrays
|
|
105
|
+
const result = {};
|
|
106
|
+
for (const [key, val] of Object.entries(dependencies)) {
|
|
107
|
+
result[key] = Array.from(val);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Builds a complete RepositoryContext for the given directory.
|
|
113
|
+
*/
|
|
114
|
+
export async function buildRepositoryContext(workspaceRoot) {
|
|
115
|
+
const manifestHash = await calculateManifestHash(workspaceRoot);
|
|
116
|
+
// 1. Detect modules
|
|
117
|
+
const detectedModules = await detectProjectModules(workspaceRoot);
|
|
118
|
+
const moduleNames = detectedModules.map((m) => m.name === "." ? "root" : m.name);
|
|
119
|
+
// Analyze import boundaries
|
|
120
|
+
const importGraph = await analyzeImportGraph(workspaceRoot, detectedModules.map((m) => m.name));
|
|
121
|
+
const moduleBoundaries = detectedModules.map((m) => {
|
|
122
|
+
const name = m.name === "." ? "root" : m.name;
|
|
123
|
+
const dependencies = importGraph[m.name] || [];
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
path: m.dir,
|
|
127
|
+
dependencies,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
// Determine main stack & architecture
|
|
131
|
+
let mainStack = "custom";
|
|
132
|
+
const stackCounts = {};
|
|
133
|
+
for (const m of detectedModules) {
|
|
134
|
+
for (const s of m.stacks) {
|
|
135
|
+
stackCounts[s] = (stackCounts[s] || 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const sortedStacks = Object.entries(stackCounts).sort((a, b) => b[1] - a[1]);
|
|
139
|
+
if (sortedStacks.length > 0) {
|
|
140
|
+
mainStack = sortedStacks[0][0];
|
|
141
|
+
}
|
|
142
|
+
// Determine default architecture style based on stack
|
|
143
|
+
let architecture = "layered";
|
|
144
|
+
if (mainStack === "spring-boot") {
|
|
145
|
+
architecture = "clean-architecture";
|
|
146
|
+
}
|
|
147
|
+
else if (mainStack === "react-ts") {
|
|
148
|
+
architecture = "feature-first";
|
|
149
|
+
}
|
|
150
|
+
else if (mainStack === "fastapi") {
|
|
151
|
+
architecture = "vertical-slice";
|
|
152
|
+
}
|
|
153
|
+
// Testing strategy defaults
|
|
154
|
+
const frameworks = [];
|
|
155
|
+
if (mainStack === "react-ts")
|
|
156
|
+
frameworks.push("vitest", "testing-library");
|
|
157
|
+
else if (mainStack === "spring-boot")
|
|
158
|
+
frameworks.push("junit", "mockito");
|
|
159
|
+
else if (mainStack === "fastapi")
|
|
160
|
+
frameworks.push("pytest");
|
|
161
|
+
const testing = {
|
|
162
|
+
frameworks,
|
|
163
|
+
coverageGoal: 80,
|
|
164
|
+
};
|
|
165
|
+
// Validation libraries defaults
|
|
166
|
+
const validationLibraries = [];
|
|
167
|
+
if (mainStack === "spring-boot") {
|
|
168
|
+
validationLibraries.push("jakarta.validation");
|
|
169
|
+
}
|
|
170
|
+
else if (mainStack === "react-ts") {
|
|
171
|
+
validationLibraries.push("zod");
|
|
172
|
+
}
|
|
173
|
+
else if (mainStack === "fastapi") {
|
|
174
|
+
validationLibraries.push("pydantic");
|
|
175
|
+
}
|
|
176
|
+
const registry = await loadAWOSPlugins(workspaceRoot);
|
|
177
|
+
let context = {
|
|
178
|
+
stack: mainStack,
|
|
179
|
+
architecture,
|
|
180
|
+
modules: moduleBoundaries,
|
|
181
|
+
testing,
|
|
182
|
+
validation: {
|
|
183
|
+
libraries: validationLibraries,
|
|
184
|
+
},
|
|
185
|
+
hash: manifestHash,
|
|
186
|
+
};
|
|
187
|
+
for (const analyzer of registry.analyzers) {
|
|
188
|
+
try {
|
|
189
|
+
const partial = await analyzer.detect(workspaceRoot);
|
|
190
|
+
context = {
|
|
191
|
+
...context,
|
|
192
|
+
...partial,
|
|
193
|
+
testing: {
|
|
194
|
+
...context.testing,
|
|
195
|
+
...(partial.testing || {}),
|
|
196
|
+
},
|
|
197
|
+
validation: {
|
|
198
|
+
...context.validation,
|
|
199
|
+
...(partial.validation || {}),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
console.warn(`[AWOS Intelligence] Custom analyzer failed:`, err);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return context;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Loads the RepositoryContext, using cached results if valid.
|
|
211
|
+
*/
|
|
212
|
+
export async function getRepositoryContext(workspaceRoot) {
|
|
213
|
+
const currentHash = await calculateManifestHash(workspaceRoot);
|
|
214
|
+
const fullCachePath = path.join(workspaceRoot, CACHE_FILE);
|
|
215
|
+
try {
|
|
216
|
+
const cacheContent = await fs.readFile(fullCachePath, "utf8");
|
|
217
|
+
const cachedCtx = JSON.parse(cacheContent);
|
|
218
|
+
if (cachedCtx.hash === currentHash) {
|
|
219
|
+
return cachedCtx;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Cache miss or read failure
|
|
224
|
+
}
|
|
225
|
+
// Re-build and write to cache
|
|
226
|
+
const context = await buildRepositoryContext(workspaceRoot);
|
|
227
|
+
try {
|
|
228
|
+
await fs.mkdir(path.dirname(fullCachePath), { recursive: true });
|
|
229
|
+
await fs.writeFile(fullCachePath, JSON.stringify(context, null, 2), "utf8");
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Non-blocking cache write failure
|
|
233
|
+
}
|
|
234
|
+
return context;
|
|
235
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { parseImports } from "../parser.js";
|
|
8
|
+
// Built-in Profiles database fallback
|
|
9
|
+
const BUILTIN_PROFILES = {
|
|
10
|
+
"layered": {
|
|
11
|
+
name: "layered",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
folderStructure: [
|
|
14
|
+
{
|
|
15
|
+
pathPattern: "**/controller/**",
|
|
16
|
+
allowedImports: ["**/service/**", "**/dto/**", "**/model/**"],
|
|
17
|
+
forbiddenImports: ["**/repository/**", "**/entity/**"],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
pathPattern: "**/service/**",
|
|
21
|
+
allowedImports: ["**/repository/**", "**/dto/**", "**/model/**", "**/entity/**"],
|
|
22
|
+
forbiddenImports: ["**/controller/**"],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
reviewRules: {
|
|
26
|
+
maxFileLines: 500,
|
|
27
|
+
maxMethodLines: 50,
|
|
28
|
+
requireInterfaceForServices: false,
|
|
29
|
+
},
|
|
30
|
+
testingRequirements: {
|
|
31
|
+
mustHaveTestFile: false,
|
|
32
|
+
namingSuffix: "Test.java",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
"clean-architecture": {
|
|
36
|
+
name: "clean-architecture",
|
|
37
|
+
version: "1.0.0",
|
|
38
|
+
extends: "layered",
|
|
39
|
+
folderStructure: [
|
|
40
|
+
{
|
|
41
|
+
pathPattern: "**/domain/**",
|
|
42
|
+
allowedImports: [],
|
|
43
|
+
forbiddenImports: ["**/infrastructure/**", "**/application/**", "**/presentation/**"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
pathPattern: "**/application/**",
|
|
47
|
+
allowedImports: ["**/domain/**"],
|
|
48
|
+
forbiddenImports: ["**/infrastructure/**", "**/presentation/**"],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
reviewRules: {
|
|
52
|
+
maxFileLines: 300,
|
|
53
|
+
maxMethodLines: 30,
|
|
54
|
+
requireInterfaceForServices: true,
|
|
55
|
+
},
|
|
56
|
+
testingRequirements: {
|
|
57
|
+
mustHaveTestFile: true,
|
|
58
|
+
namingSuffix: "Test.java",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
"feature-first": {
|
|
62
|
+
name: "feature-first",
|
|
63
|
+
version: "1.0.0",
|
|
64
|
+
folderStructure: [
|
|
65
|
+
{
|
|
66
|
+
pathPattern: "**/features/*/**",
|
|
67
|
+
mustContainPatterns: ["components", "hooks", "index.ts"],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
reviewRules: {
|
|
71
|
+
maxFileLines: 400,
|
|
72
|
+
maxMethodLines: 40,
|
|
73
|
+
requireInterfaceForServices: false,
|
|
74
|
+
},
|
|
75
|
+
testingRequirements: {
|
|
76
|
+
mustHaveTestFile: true,
|
|
77
|
+
namingSuffix: ".test.ts",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Deep merges two architecture profiles for inheritance resolving.
|
|
83
|
+
*/
|
|
84
|
+
export function mergeProfiles(base, extension) {
|
|
85
|
+
return {
|
|
86
|
+
name: extension.name,
|
|
87
|
+
version: extension.version,
|
|
88
|
+
extends: extension.extends,
|
|
89
|
+
folderStructure: [...(base.folderStructure || []), ...(extension.folderStructure || [])],
|
|
90
|
+
reviewRules: {
|
|
91
|
+
...base.reviewRules,
|
|
92
|
+
...extension.reviewRules,
|
|
93
|
+
},
|
|
94
|
+
testingRequirements: {
|
|
95
|
+
...base.testingRequirements,
|
|
96
|
+
...extension.testingRequirements,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Loads a profile by name from disk or built-in registry.
|
|
102
|
+
*/
|
|
103
|
+
export async function loadProfile(profileName, profilesDir) {
|
|
104
|
+
const { loadAWOSPlugins } = await import("./registry.js");
|
|
105
|
+
const registry = await loadAWOSPlugins(process.cwd());
|
|
106
|
+
let profile = registry.profiles.get(profileName) || BUILTIN_PROFILES[profileName];
|
|
107
|
+
if (profilesDir) {
|
|
108
|
+
try {
|
|
109
|
+
const customPath = path.join(profilesDir, `${profileName}.json`);
|
|
110
|
+
const fileContent = await fs.readFile(customPath, "utf8");
|
|
111
|
+
profile = JSON.parse(fileContent);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Fallback to built-in if file read fails
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!profile) {
|
|
118
|
+
throw new Error(`Architecture Profile '${profileName}' not found.`);
|
|
119
|
+
}
|
|
120
|
+
if (profile.extends) {
|
|
121
|
+
const parentProfile = await loadProfile(profile.extends, profilesDir);
|
|
122
|
+
return mergeProfiles(parentProfile, profile);
|
|
123
|
+
}
|
|
124
|
+
return profile;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Matches glob-ish path patterns. Simple helper.
|
|
128
|
+
*/
|
|
129
|
+
function matchPathPattern(filePath, pattern) {
|
|
130
|
+
// Translate simple **/ pattern to regex
|
|
131
|
+
const regexString = pattern
|
|
132
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape regex specials
|
|
133
|
+
.replace(/\*\*/g, ".*")
|
|
134
|
+
.replace(/\*/g, "[^/]*");
|
|
135
|
+
const regex = new RegExp(`^${regexString}$|^${regexString}`);
|
|
136
|
+
return regex.test(filePath.replace(/\\/g, "/"));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Validates a single file content and path against architectural rules.
|
|
140
|
+
*/
|
|
141
|
+
export async function validateFile(filePath, content, profile) {
|
|
142
|
+
const violations = [];
|
|
143
|
+
const relativePath = filePath.replace(/\\/g, "/");
|
|
144
|
+
// 1. Line limits check
|
|
145
|
+
const lines = content.split("\n");
|
|
146
|
+
if (lines.length > profile.reviewRules.maxFileLines) {
|
|
147
|
+
violations.push({
|
|
148
|
+
filePath,
|
|
149
|
+
ruleName: "maxFileLines",
|
|
150
|
+
message: `File has ${lines.length} lines, exceeding the limit of ${profile.reviewRules.maxFileLines}.`,
|
|
151
|
+
severity: "error",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// 2. Folder structures and imports validation
|
|
155
|
+
for (const rule of profile.folderStructure) {
|
|
156
|
+
if (matchPathPattern(relativePath, rule.pathPattern)) {
|
|
157
|
+
// Check imports
|
|
158
|
+
if (rule.forbiddenImports || rule.allowedImports) {
|
|
159
|
+
const parsedImports = await parseImports(filePath, content);
|
|
160
|
+
for (const importedRef of parsedImports) {
|
|
161
|
+
// Forbidden check
|
|
162
|
+
if (rule.forbiddenImports) {
|
|
163
|
+
for (const forbidden of rule.forbiddenImports) {
|
|
164
|
+
if (matchPathPattern(importedRef, forbidden)) {
|
|
165
|
+
violations.push({
|
|
166
|
+
filePath,
|
|
167
|
+
ruleName: "forbiddenImports",
|
|
168
|
+
message: `Importing '${importedRef}' is forbidden in '${rule.pathPattern}'.`,
|
|
169
|
+
severity: "error",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Allowed check (if specified, anything not matched is forbidden)
|
|
175
|
+
if (rule.allowedImports && rule.allowedImports.length > 0) {
|
|
176
|
+
let isAllowed = false;
|
|
177
|
+
// Also allow standard library / node modules
|
|
178
|
+
if (!importedRef.startsWith(".") && !importedRef.startsWith("/") && !importedRef.includes("/")) {
|
|
179
|
+
isAllowed = true;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
for (const allowed of rule.allowedImports) {
|
|
183
|
+
if (matchPathPattern(importedRef, allowed)) {
|
|
184
|
+
isAllowed = true;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!isAllowed) {
|
|
190
|
+
violations.push({
|
|
191
|
+
filePath,
|
|
192
|
+
ruleName: "allowedImports",
|
|
193
|
+
message: `Importing '${importedRef}' is not explicitly allowed in '${rule.pathPattern}'.`,
|
|
194
|
+
severity: "error",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Check required patterns
|
|
201
|
+
if (rule.mustContainPatterns) {
|
|
202
|
+
for (const pattern of rule.mustContainPatterns) {
|
|
203
|
+
if (!content.includes(pattern)) {
|
|
204
|
+
violations.push({
|
|
205
|
+
filePath,
|
|
206
|
+
ruleName: "mustContainPatterns",
|
|
207
|
+
message: `File must contain pattern: '${pattern}'.`,
|
|
208
|
+
severity: "warn",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return violations;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Scans workspace and validates all source files against the ArchitectureProfile.
|
|
219
|
+
*/
|
|
220
|
+
export async function validateArchitecture(workspaceRoot, profile) {
|
|
221
|
+
const violations = [];
|
|
222
|
+
async function traverse(dir) {
|
|
223
|
+
try {
|
|
224
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const fullPath = path.join(dir, entry.name);
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
if (entry.name.startsWith(".") ||
|
|
229
|
+
entry.name === "node_modules" ||
|
|
230
|
+
entry.name === "dist" ||
|
|
231
|
+
entry.name === "build" ||
|
|
232
|
+
entry.name === "target") {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
await traverse(fullPath);
|
|
236
|
+
}
|
|
237
|
+
else if (entry.isFile() && /\.(ts|tsx|java|py)$/.test(entry.name)) {
|
|
238
|
+
// Avoid validating test files for structure limits
|
|
239
|
+
const isTest = entry.name.endsWith(profile.testingRequirements.namingSuffix);
|
|
240
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
241
|
+
if (!isTest) {
|
|
242
|
+
const fileViolations = await validateFile(fullPath, content, profile);
|
|
243
|
+
violations.push(...fileViolations);
|
|
244
|
+
// Verify test file existence if required
|
|
245
|
+
if (profile.testingRequirements.mustHaveTestFile) {
|
|
246
|
+
const ext = path.extname(entry.name);
|
|
247
|
+
const baseName = path.basename(entry.name, ext);
|
|
248
|
+
const testFileName = baseName + profile.testingRequirements.namingSuffix;
|
|
249
|
+
const testPath = path.join(dir, testFileName);
|
|
250
|
+
try {
|
|
251
|
+
await fs.stat(testPath);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
violations.push({
|
|
255
|
+
filePath: fullPath,
|
|
256
|
+
ruleName: "mustHaveTestFile",
|
|
257
|
+
message: `Missing matching test file: '${testFileName}' expected in the same directory.`,
|
|
258
|
+
severity: "warn",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Ignore read errors
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
await traverse(workspaceRoot);
|
|
271
|
+
return violations;
|
|
272
|
+
}
|