claudeos-core 1.2.4 → 1.3.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.
Potentially problematic release.
This version of claudeos-core might be problematic. Click here for more details.
- package/CHANGELOG.md +76 -0
- package/README.de.md +50 -10
- package/README.es.md +51 -10
- package/README.fr.md +51 -10
- package/README.hi.md +51 -10
- package/README.ja.md +52 -10
- package/README.ko.md +51 -10
- package/README.md +62 -15
- package/README.ru.md +51 -10
- package/README.vi.md +51 -10
- package/README.zh-CN.md +51 -10
- package/bin/cli.js +171 -36
- package/bootstrap.sh +71 -23
- package/content-validator/index.js +16 -13
- package/health-checker/index.js +4 -3
- package/lib/safe-fs.js +110 -0
- package/manifest-generator/index.js +13 -7
- package/package.json +4 -2
- package/pass-json-validator/index.js +3 -5
- package/pass-prompts/templates/java-spring/pass1.md +4 -1
- package/pass-prompts/templates/java-spring/pass2.md +3 -3
- package/pass-prompts/templates/java-spring/pass3.md +42 -5
- package/pass-prompts/templates/kotlin-spring/pass1.md +4 -1
- package/pass-prompts/templates/kotlin-spring/pass2.md +5 -5
- package/pass-prompts/templates/kotlin-spring/pass3.md +42 -5
- package/pass-prompts/templates/node-express/pass1.md +4 -1
- package/pass-prompts/templates/node-express/pass2.md +4 -1
- package/pass-prompts/templates/node-express/pass3.md +44 -6
- package/pass-prompts/templates/node-nextjs/pass1.md +14 -4
- package/pass-prompts/templates/node-nextjs/pass2.md +6 -4
- package/pass-prompts/templates/node-nextjs/pass3.md +45 -6
- package/pass-prompts/templates/python-django/pass1.md +4 -2
- package/pass-prompts/templates/python-django/pass2.md +4 -4
- package/pass-prompts/templates/python-django/pass3.md +42 -5
- package/pass-prompts/templates/python-fastapi/pass1.md +4 -1
- package/pass-prompts/templates/python-fastapi/pass2.md +4 -4
- package/pass-prompts/templates/python-fastapi/pass3.md +42 -5
- package/plan-installer/domain-grouper.js +74 -0
- package/plan-installer/index.js +35 -1305
- package/plan-installer/prompt-generator.js +94 -0
- package/plan-installer/stack-detector.js +326 -0
- package/plan-installer/structure-scanner.js +783 -0
- package/plan-validator/index.js +84 -20
- package/sync-checker/index.js +7 -3
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Structure Scanner
|
|
3
|
+
*
|
|
4
|
+
* Scans project directory structure to discover domains (backend + frontend).
|
|
5
|
+
* Supports: Java (5 patterns), Kotlin (multi-module + CQRS), Node.js, Python.
|
|
6
|
+
* Also resolves shared query modules for Kotlin monorepos.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { glob } = require("glob");
|
|
11
|
+
|
|
12
|
+
async function scanStructure(stack, ROOT) {
|
|
13
|
+
const backendDomains = [];
|
|
14
|
+
const frontendDomains = [];
|
|
15
|
+
let rootPackage = null;
|
|
16
|
+
|
|
17
|
+
// ── Java ──
|
|
18
|
+
if (stack.language === "java") {
|
|
19
|
+
const javaFiles = await glob("src/main/java/**/*.java", { cwd: ROOT });
|
|
20
|
+
for (const f of javaFiles) {
|
|
21
|
+
const m = f.match(/src\/main\/java\/(.+?)\/(controller|service|mapper|dto|entity|repository|adapter)/);
|
|
22
|
+
if (m) { rootPackage = m[1].replace(/\//g, "."); break; }
|
|
23
|
+
}
|
|
24
|
+
const domainMap = {};
|
|
25
|
+
let detectedPattern = null;
|
|
26
|
+
|
|
27
|
+
// Pattern A: controller/{domain}/*.java (layer-first — domain under controller)
|
|
28
|
+
const controllersA = await glob("src/main/java/**/controller/*/*.java", { cwd: ROOT });
|
|
29
|
+
for (const f of controllersA) {
|
|
30
|
+
const m = f.match(/controller\/([^/]+)\//);
|
|
31
|
+
if (m) {
|
|
32
|
+
const d = m[1];
|
|
33
|
+
if (!domainMap[d]) domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "A" };
|
|
34
|
+
domainMap[d].controllers++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (Object.keys(domainMap).length > 0) detectedPattern = "A";
|
|
38
|
+
|
|
39
|
+
// Pattern B/D: {domain}/controller/*.java (domain-first — controller under domain)
|
|
40
|
+
// D extends B: {module}/{domain}/controller/ — auto-upgrade to module/domain on name conflict
|
|
41
|
+
if (!detectedPattern) {
|
|
42
|
+
const controllersB = await glob("src/main/java/**/*/controller/*.java", { cwd: ROOT });
|
|
43
|
+
const domainPaths = {};
|
|
44
|
+
for (const f of controllersB) {
|
|
45
|
+
const m = f.match(/\/([^/]+)\/controller\/[^/]+\.java$/);
|
|
46
|
+
if (m) {
|
|
47
|
+
const d = m[1];
|
|
48
|
+
const parentMatch = f.match(/\/([^/]+)\/([^/]+)\/controller\//);
|
|
49
|
+
const parentModule = parentMatch ? parentMatch[1] : null;
|
|
50
|
+
if (!domainPaths[d]) domainPaths[d] = [];
|
|
51
|
+
domainPaths[d].push({ file: f, module: parentModule });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If same domain name found in multiple modules, use module/domain form (Pattern D)
|
|
56
|
+
for (const [d, entries] of Object.entries(domainPaths)) {
|
|
57
|
+
const modules = [...new Set(entries.map(e => e.module).filter(Boolean))];
|
|
58
|
+
if (modules.length > 1) {
|
|
59
|
+
// Pattern D: conflict — register as module/domain
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const fullName = entry.module ? `${entry.module}/${d}` : d;
|
|
62
|
+
if (!domainMap[fullName]) domainMap[fullName] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "D", modulePath: entry.module, domainName: d };
|
|
63
|
+
domainMap[fullName].controllers++;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
if (!domainMap[d]) domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "B" };
|
|
67
|
+
domainMap[d].controllers += entries.length;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (Object.keys(domainMap).length > 0) detectedPattern = domainMap[Object.keys(domainMap)[0]].pattern;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Pattern E: DDD/Hexagonal — {domain}/adapter/in/web/*.java or {domain}/adapter/in/rest/*.java
|
|
74
|
+
if (!detectedPattern) {
|
|
75
|
+
const controllersE = await glob("src/main/java/**/adapter/in/{web,rest}/*.java", { cwd: ROOT });
|
|
76
|
+
for (const f of controllersE) {
|
|
77
|
+
const m = f.match(/\/([^/]+)\/adapter\/in\/(web|rest)\/[^/]+\.java$/);
|
|
78
|
+
if (m) {
|
|
79
|
+
const d = m[1];
|
|
80
|
+
if (!domainMap[d]) domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "E" };
|
|
81
|
+
domainMap[d].controllers++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (Object.keys(domainMap).length > 0) detectedPattern = "E";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Pattern C: Flat structure — controller/*.java (no domain directory, extract domain from class name)
|
|
88
|
+
if (!detectedPattern) {
|
|
89
|
+
const controllersC = await glob("src/main/java/**/controller/*.java", { cwd: ROOT });
|
|
90
|
+
for (const f of controllersC) {
|
|
91
|
+
const m = f.match(/\/([A-Z][a-zA-Z]*)Controller\.java$/);
|
|
92
|
+
if (m) {
|
|
93
|
+
const d = m[1].toLowerCase();
|
|
94
|
+
if (!domainMap[d]) domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "C" };
|
|
95
|
+
domainMap[d].controllers++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (Object.keys(domainMap).length > 0) detectedPattern = "C";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Supplementary scan: detect service-only domains without controllers ──
|
|
102
|
+
// Catch domains like core/delivery that have service/mapper but no controller
|
|
103
|
+
if (detectedPattern === "B" || detectedPattern === "D" || !detectedPattern) {
|
|
104
|
+
const serviceDirs = await glob("src/main/java/**/*/service/*.java", { cwd: ROOT });
|
|
105
|
+
const mapperDirs = await glob("src/main/java/**/*/{mapper,repository}/*.java", { cwd: ROOT });
|
|
106
|
+
const allServiceFiles = [...serviceDirs, ...mapperDirs];
|
|
107
|
+
const skipDomains = ["common", "config", "util", "utils", "base", "core", "shared", "global", "framework", "infra", "front", "admin", "back", "internal", "external", "web", "app", "test", "tests", "main", "generated", "build"];
|
|
108
|
+
for (const f of allServiceFiles) {
|
|
109
|
+
const m = f.match(/\/([^/]+)\/(service|mapper|repository)\/[^/]+\.java$/);
|
|
110
|
+
if (m) {
|
|
111
|
+
const d = m[1];
|
|
112
|
+
if (!domainMap[d] && !skipDomains.includes(d) && !/^v\d+$/.test(d)) {
|
|
113
|
+
domainMap[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: detectedPattern || "B" };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Scan service/mapper/dto/xml files for each domain
|
|
120
|
+
for (const d of Object.keys(domainMap)) {
|
|
121
|
+
const p = domainMap[d].pattern;
|
|
122
|
+
const dn = domainMap[d].domainName || d;
|
|
123
|
+
let svcGlob, mprGlob, dtoGlob;
|
|
124
|
+
|
|
125
|
+
if (p === "A") {
|
|
126
|
+
svcGlob = `src/main/java/**/service/${d}/*.java`;
|
|
127
|
+
mprGlob = `src/main/java/**/{mapper,repository}/${d}/*.java`;
|
|
128
|
+
dtoGlob = `src/main/java/**/dto/${d}/**/*.java`;
|
|
129
|
+
} else if (p === "B" || p === "D") {
|
|
130
|
+
svcGlob = `src/main/java/**/${dn}/service/*.java`;
|
|
131
|
+
mprGlob = `src/main/java/**/${dn}/{mapper,repository}/*.java`;
|
|
132
|
+
dtoGlob = `src/main/java/**/${dn}/dto/**/*.java`;
|
|
133
|
+
} else if (p === "E") {
|
|
134
|
+
svcGlob = `src/main/java/**/${d}/{application,domain}/**/*.java`;
|
|
135
|
+
mprGlob = `src/main/java/**/${d}/adapter/out/{persistence,repository}/*.java`;
|
|
136
|
+
dtoGlob = `src/main/java/**/${d}/**/{dto,command,query}/**/*.java`;
|
|
137
|
+
} else {
|
|
138
|
+
// Pattern C: Flat — match domain name from file name
|
|
139
|
+
const cap = d.charAt(0).toUpperCase() + d.slice(1);
|
|
140
|
+
svcGlob = `src/main/java/**/service/${cap}*.java`;
|
|
141
|
+
mprGlob = `src/main/java/**/{mapper,repository}/${cap}*.java`;
|
|
142
|
+
dtoGlob = `src/main/java/**/dto/${cap}*.java`;
|
|
143
|
+
}
|
|
144
|
+
const xmlGlob = `src/main/resources/mapper/**/${dn}/*.xml`;
|
|
145
|
+
|
|
146
|
+
const svc = await glob(svcGlob, { cwd: ROOT });
|
|
147
|
+
const mpr = await glob(mprGlob, { cwd: ROOT });
|
|
148
|
+
const dto = await glob(dtoGlob, { cwd: ROOT });
|
|
149
|
+
const xml = await glob(xmlGlob, { cwd: ROOT });
|
|
150
|
+
domainMap[d].services = svc.length;
|
|
151
|
+
domainMap[d].mappers = mpr.length;
|
|
152
|
+
domainMap[d].dtos = dto.length;
|
|
153
|
+
domainMap[d].xmlMappers = xml.length;
|
|
154
|
+
backendDomains.push({ name: d, type: "backend", ...domainMap[d], totalFiles: svc.length + mpr.length + dto.length + xml.length + domainMap[d].controllers });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Java fallback: extract domains directly from all .java files when glob returns 0 ──
|
|
158
|
+
if (backendDomains.length === 0) {
|
|
159
|
+
const allJava = await glob("**/*.java", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**", "**/target/**", "**/test/**", "**/generated/**"] });
|
|
160
|
+
const javaDomains = {};
|
|
161
|
+
const skipNames = ["common", "config", "util", "utils", "base", "shared", "global", "framework", "infra", "api", "main", "front", "admin", "back", "internal", "external", "web", "app", "test", "tests", "generated", "build"];
|
|
162
|
+
const versionPattern = /^v\d+$/;
|
|
163
|
+
const layerNames = ["controller", "service", "mapper", "repository", "dao", "dto", "vo", "entity", "aggregator", "adapter"];
|
|
164
|
+
|
|
165
|
+
for (const f of allJava) {
|
|
166
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
167
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
168
|
+
if (layerNames.includes(parts[i]) && i > 0) {
|
|
169
|
+
// Pattern A detection: .../{domain}/controller/... or .../controller/{domain}/...
|
|
170
|
+
const prevDir = parts[i - 1];
|
|
171
|
+
const nextDir = parts[i + 1];
|
|
172
|
+
|
|
173
|
+
// {domain}/layer/ pattern (domain before layer)
|
|
174
|
+
if (!skipNames.includes(prevDir) && !layerNames.includes(prevDir) && !prevDir.includes(".") && !versionPattern.test(prevDir)) {
|
|
175
|
+
if (!javaDomains[prevDir]) javaDomains[prevDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "B" };
|
|
176
|
+
if (parts[i] === "controller") javaDomains[prevDir].controllers++;
|
|
177
|
+
else if (parts[i] === "service") javaDomains[prevDir].services++;
|
|
178
|
+
else if (["mapper", "repository", "dao"].includes(parts[i])) javaDomains[prevDir].mappers++;
|
|
179
|
+
else if (["dto", "vo"].includes(parts[i])) javaDomains[prevDir].dtos++;
|
|
180
|
+
}
|
|
181
|
+
// layer/{domain}/ pattern (layer before domain)
|
|
182
|
+
if (nextDir && !nextDir.endsWith(".java") && !skipNames.includes(nextDir) && !layerNames.includes(nextDir) && !versionPattern.test(nextDir)) {
|
|
183
|
+
if (!javaDomains[nextDir]) javaDomains[nextDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "A" };
|
|
184
|
+
if (parts[i] === "controller") javaDomains[nextDir].controllers++;
|
|
185
|
+
else if (parts[i] === "service") javaDomains[nextDir].services++;
|
|
186
|
+
else if (["mapper", "repository", "dao"].includes(parts[i])) javaDomains[nextDir].mappers++;
|
|
187
|
+
else if (["dto", "vo"].includes(parts[i])) javaDomains[nextDir].dtos++;
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const [d, data] of Object.entries(javaDomains)) {
|
|
195
|
+
const total = data.controllers + data.services + data.mappers + data.dtos;
|
|
196
|
+
if (total > 0) {
|
|
197
|
+
backendDomains.push({ name: d, type: "backend", ...data, totalFiles: total });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Kotlin (multi-module monorepo + CQRS support) ──
|
|
204
|
+
if (stack.language === "kotlin") {
|
|
205
|
+
// Scan all .kt files across all submodules
|
|
206
|
+
const ktFiles = await glob("**/src/main/kotlin/**/*.kt", {
|
|
207
|
+
cwd: ROOT,
|
|
208
|
+
ignore: ["**/node_modules/**", "**/build/**", "**/test/**", "**/generated/**"],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Detect root package from first controller/service file
|
|
212
|
+
for (const f of ktFiles) {
|
|
213
|
+
const m = f.match(/src\/main\/kotlin\/(.+?)\/(controller|service|mapper|dto|entity|repository|adapter)/);
|
|
214
|
+
if (m) { rootPackage = m[1].replace(/\//g, "."); break; }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Detect modules from directory structure (servers/*/ or direct submodules)
|
|
218
|
+
const moduleGlobs = [
|
|
219
|
+
"servers/*/*/src/main/kotlin/", // servers/command/reservation-command-server/
|
|
220
|
+
"servers/*/src/main/kotlin/", // servers/iam-server/
|
|
221
|
+
"*/src/main/kotlin/", // shared-lib/ or integration-lib/
|
|
222
|
+
];
|
|
223
|
+
const moduleSet = {};
|
|
224
|
+
for (const mg of moduleGlobs) {
|
|
225
|
+
const moduleDirs = await glob(mg, { cwd: ROOT });
|
|
226
|
+
for (const md of moduleDirs) {
|
|
227
|
+
const parts = md.replace(/\/src\/main\/kotlin\/$/, "").split("/");
|
|
228
|
+
const moduleName = parts[parts.length - 1]; // e.g., "reservation-command-server"
|
|
229
|
+
if (moduleSet[moduleName]) {
|
|
230
|
+
// Name conflict: use full relative path as key to avoid silent loss
|
|
231
|
+
const fullKey = md.replace(/\/src\/main\/kotlin\/$/, "").replace(/\//g, "__");
|
|
232
|
+
moduleSet[fullKey] = md;
|
|
233
|
+
} else {
|
|
234
|
+
moduleSet[moduleName] = md;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Categorize modules and extract domains
|
|
240
|
+
const skipModules = ["build", "gradle", "buildSrc", ".gradle", "node_modules"];
|
|
241
|
+
const serverTypes = { command: "command", query: "query", bff: "bff", integration: "integration" };
|
|
242
|
+
const domainMap = {};
|
|
243
|
+
|
|
244
|
+
for (const [moduleName, modulePath] of Object.entries(moduleSet)) {
|
|
245
|
+
if (skipModules.some(s => moduleName.includes(s))) continue;
|
|
246
|
+
|
|
247
|
+
// Determine server type from module name
|
|
248
|
+
let serverType = "standalone";
|
|
249
|
+
for (const [key, val] of Object.entries(serverTypes)) {
|
|
250
|
+
if (moduleName.includes(key)) { serverType = val; break; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Extract domain name from module name
|
|
254
|
+
// e.g., "reservation-command-server" → "reservation"
|
|
255
|
+
// e.g., "pms-bff-server" → "pms"
|
|
256
|
+
// e.g., "iam-server" → "iam"
|
|
257
|
+
// e.g., "shared-lib" → "shared-lib"
|
|
258
|
+
// e.g., "common-query-server" → "common-query" (shared server, not a domain named "common")
|
|
259
|
+
let domainName = moduleName
|
|
260
|
+
.replace(/-(?:command|query|bff|integration|adapter)-server$/, "")
|
|
261
|
+
.replace(/-server$/, "");
|
|
262
|
+
|
|
263
|
+
// Guard: if domain extraction resulted in a generic name that is likely a shared module
|
|
264
|
+
// (e.g., "common-query-server" → "common"), keep the full type qualifier
|
|
265
|
+
const genericNames = ["common", "shared", "base", "core", "global", "internal"];
|
|
266
|
+
if (genericNames.includes(domainName) && serverType !== "standalone") {
|
|
267
|
+
domainName = `${domainName}-${serverType}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Scan files in this module (append "/" to prevent prefix overlap: e.g., reservation vs reservation-ext)
|
|
271
|
+
const modulePrefix = modulePath.replace(/\/$/, "").replace(/\/src\/main\/kotlin$/, "") + "/";
|
|
272
|
+
const moduleKtFiles = ktFiles.filter(f => f.startsWith(modulePrefix) || f === modulePrefix.slice(0, -1));
|
|
273
|
+
const controllers = moduleKtFiles.filter(f => /controller/i.test(f)).length;
|
|
274
|
+
const services = moduleKtFiles.filter(f => /service/i.test(f)).length;
|
|
275
|
+
const repositories = moduleKtFiles.filter(f => /repository|mapper/i.test(f)).length;
|
|
276
|
+
const dtos = moduleKtFiles.filter(f => /dto|vo|request|response|model/i.test(f) && !/repository|service|controller/i.test(f)).length;
|
|
277
|
+
const totalFiles = moduleKtFiles.length;
|
|
278
|
+
|
|
279
|
+
if (totalFiles === 0) continue;
|
|
280
|
+
|
|
281
|
+
const key = `${domainName}:${serverType}`;
|
|
282
|
+
domainMap[key] = {
|
|
283
|
+
name: domainName,
|
|
284
|
+
moduleName,
|
|
285
|
+
modulePath,
|
|
286
|
+
serverType,
|
|
287
|
+
controllers,
|
|
288
|
+
services,
|
|
289
|
+
repositories,
|
|
290
|
+
dtos,
|
|
291
|
+
totalFiles,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Group by domain: merge command/query/bff of same domain
|
|
296
|
+
const domainGroups = {};
|
|
297
|
+
for (const [key, info] of Object.entries(domainMap)) {
|
|
298
|
+
const { name } = info;
|
|
299
|
+
if (!domainGroups[name]) {
|
|
300
|
+
domainGroups[name] = { name, type: "backend", modules: [], controllers: 0, services: 0, repositories: 0, dtos: 0, totalFiles: 0, serverTypes: [] };
|
|
301
|
+
}
|
|
302
|
+
const dg = domainGroups[name];
|
|
303
|
+
dg.modules.push(info.moduleName);
|
|
304
|
+
dg.controllers += info.controllers;
|
|
305
|
+
dg.services += info.services;
|
|
306
|
+
dg.repositories += info.repositories;
|
|
307
|
+
dg.dtos += info.dtos;
|
|
308
|
+
dg.totalFiles += info.totalFiles;
|
|
309
|
+
if (!dg.serverTypes.includes(info.serverType)) dg.serverTypes.push(info.serverType);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Push to backendDomains
|
|
313
|
+
for (const dg of Object.values(domainGroups)) {
|
|
314
|
+
backendDomains.push({
|
|
315
|
+
name: dg.name,
|
|
316
|
+
type: "backend",
|
|
317
|
+
controllers: dg.controllers,
|
|
318
|
+
services: dg.services,
|
|
319
|
+
mappers: dg.repositories,
|
|
320
|
+
dtos: dg.dtos,
|
|
321
|
+
totalFiles: dg.totalFiles,
|
|
322
|
+
modules: dg.modules,
|
|
323
|
+
serverTypes: dg.serverTypes,
|
|
324
|
+
pattern: "kotlin-multimodule",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Also scan shared libraries as special domains
|
|
329
|
+
const libDirs = await glob("{shared-lib,integration-lib,*-lib}/src/main/kotlin/", { cwd: ROOT });
|
|
330
|
+
for (const ld of libDirs) {
|
|
331
|
+
const libName = ld.split("/")[0];
|
|
332
|
+
if (domainGroups[libName]) continue; // Already captured
|
|
333
|
+
const libFiles = ktFiles.filter(f => f.startsWith(libName + "/"));
|
|
334
|
+
if (libFiles.length > 0) {
|
|
335
|
+
backendDomains.push({
|
|
336
|
+
name: libName,
|
|
337
|
+
type: "backend",
|
|
338
|
+
controllers: 0,
|
|
339
|
+
services: libFiles.filter(f => /service/i.test(f)).length,
|
|
340
|
+
mappers: 0,
|
|
341
|
+
dtos: libFiles.filter(f => /dto|vo|model/i.test(f)).length,
|
|
342
|
+
totalFiles: libFiles.length,
|
|
343
|
+
modules: [libName],
|
|
344
|
+
serverTypes: ["library"],
|
|
345
|
+
pattern: "kotlin-library",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Resolve shared query modules: redistribute internal domains to actual domain entries
|
|
351
|
+
resolveSharedQueryDomains(backendDomains, ktFiles);
|
|
352
|
+
|
|
353
|
+
// ── Kotlin single-module fallback (no multi-module structure detected) ──
|
|
354
|
+
if (backendDomains.length === 0 && ktFiles.length > 0) {
|
|
355
|
+
const ktDomains = {};
|
|
356
|
+
const skipNames = ["common", "config", "util", "utils", "base", "shared", "global", "framework", "infra", "main", "generated", "build"];
|
|
357
|
+
const layerKw = ["controller", "service", "repository", "mapper", "dao", "dto", "vo", "entity", "aggregate", "adapter"];
|
|
358
|
+
for (const f of ktFiles) {
|
|
359
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
360
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
361
|
+
if (layerKw.includes(parts[i].toLowerCase())) {
|
|
362
|
+
// domain/layer/ pattern
|
|
363
|
+
if (i > 0) {
|
|
364
|
+
const d = parts[i - 1].toLowerCase();
|
|
365
|
+
if (!skipNames.includes(d) && !layerKw.includes(d) && d.length > 1 && d !== "kotlin") {
|
|
366
|
+
if (!ktDomains[d]) ktDomains[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, totalFiles: 0 };
|
|
367
|
+
if (parts[i] === "controller") ktDomains[d].controllers++;
|
|
368
|
+
else if (parts[i] === "service") ktDomains[d].services++;
|
|
369
|
+
else if (["repository", "mapper", "dao"].includes(parts[i])) ktDomains[d].mappers++;
|
|
370
|
+
else if (["dto", "vo", "entity"].includes(parts[i])) ktDomains[d].dtos++;
|
|
371
|
+
ktDomains[d].totalFiles++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const [d, data] of Object.entries(ktDomains)) {
|
|
379
|
+
if (data.totalFiles > 0) {
|
|
380
|
+
backendDomains.push({ name: d, type: "backend", ...data, pattern: "kotlin-single" });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── Node.js backend (Express/NestJS) — scan regardless of frontend presence ──
|
|
387
|
+
if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework) {
|
|
388
|
+
const nestModules = await glob("src/modules/*/", { cwd: ROOT });
|
|
389
|
+
const srcDirs = nestModules.length > 0 ? nestModules : await glob("src/*/", { cwd: ROOT });
|
|
390
|
+
const skipDirs = ["common", "shared", "config", "utils", "lib", "core", "main", "interfaces", "types", "constants", "guards", "decorators", "pipes", "filters", "interceptors"];
|
|
391
|
+
for (let dir of srcDirs) {
|
|
392
|
+
if (!dir.endsWith("/")) dir += "/";
|
|
393
|
+
const name = path.basename(dir.replace(/\/$/, ""));
|
|
394
|
+
if (skipDirs.includes(name)) continue;
|
|
395
|
+
const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{ts,js}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*"] });
|
|
396
|
+
if (files.length > 0) {
|
|
397
|
+
const controllers = files.filter(f => /controller|router|route/.test(f)).length;
|
|
398
|
+
const services = files.filter(f => /service/.test(f)).length;
|
|
399
|
+
const dtos = files.filter(f => /dto|schema|type/.test(f)).length;
|
|
400
|
+
backendDomains.push({ name, type: "backend", controllers, services, dtos, totalFiles: files.length });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Next.js/React/Vue ──
|
|
406
|
+
if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
|
|
407
|
+
// App Router / Pages Router domains
|
|
408
|
+
const allDirs = [
|
|
409
|
+
...await glob("{app,src/app}/*/", { cwd: ROOT }),
|
|
410
|
+
...await glob("{pages,src/pages}/*/", { cwd: ROOT }),
|
|
411
|
+
];
|
|
412
|
+
const skipPages = ["api", "_app", "_document", "fonts"];
|
|
413
|
+
for (const dir of allDirs) {
|
|
414
|
+
const name = path.basename(dir);
|
|
415
|
+
if (skipPages.includes(name) || name.startsWith("(") || name.startsWith("[") || name.startsWith("_") || name.startsWith(".")) continue;
|
|
416
|
+
const files = await glob(`${dir}**/*.{tsx,jsx,ts,js}`, { cwd: ROOT });
|
|
417
|
+
if (files.length > 0) {
|
|
418
|
+
const pages = files.filter(f => /page\.|index\./.test(f)).length;
|
|
419
|
+
const layouts = files.filter(f => /layout\./.test(f)).length;
|
|
420
|
+
const clientFiles = files.filter(f => /client\./.test(f)).length;
|
|
421
|
+
const serverFiles = pages + layouts;
|
|
422
|
+
const components = files.filter(f => !/page\.|layout\.|index\.|client\./.test(f)).length;
|
|
423
|
+
frontendDomains.push({
|
|
424
|
+
name, type: "frontend", pages, layouts, clientFiles, serverFiles, components, totalFiles: files.length,
|
|
425
|
+
rscPattern: clientFiles > 0 ? "RSC+Client split" : "default",
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// FSD (Feature-Sliced Design): features/*, widgets/*, entities/*
|
|
431
|
+
const fsdLayers = ["features", "widgets", "entities"];
|
|
432
|
+
for (const layer of fsdLayers) {
|
|
433
|
+
const fsdDirs = await glob(`{${layer},src/${layer}}/*/`, { cwd: ROOT });
|
|
434
|
+
for (const dir of fsdDirs) {
|
|
435
|
+
const name = path.basename(dir);
|
|
436
|
+
if (["ui", "common", "shared", "lib", "config", "index"].includes(name)) continue;
|
|
437
|
+
const files = await glob(`${dir}**/*.{tsx,jsx,ts,js}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
|
|
438
|
+
if (files.length > 0) {
|
|
439
|
+
const uiFiles = files.filter(f => /\bui\b/.test(f)).length;
|
|
440
|
+
const modelFiles = files.filter(f => /model|store|hook/.test(f)).length;
|
|
441
|
+
frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", components: uiFiles, models: modelFiles, totalFiles: files.length });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// components/* (existing)
|
|
447
|
+
const compDirs = await glob("{src/,}components/*/", { cwd: ROOT });
|
|
448
|
+
for (const dir of compDirs) {
|
|
449
|
+
const name = path.basename(dir);
|
|
450
|
+
if (["ui", "common", "shared", "layout", "icons"].includes(name)) continue;
|
|
451
|
+
const files = await glob(`${dir}**/*.{tsx,jsx}`, { cwd: ROOT });
|
|
452
|
+
if (files.length >= 2) {
|
|
453
|
+
frontendDomains.push({ name: `comp-${name}`, type: "frontend", components: files.length, totalFiles: files.length });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Fallback: extract domains directly from page.tsx/index.tsx locations when scanners return 0 ──
|
|
458
|
+
if (frontendDomains.length === 0) {
|
|
459
|
+
const pageFiles = await glob("**/page.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
460
|
+
const domainSet = {};
|
|
461
|
+
const skipNames = ["app", "src", "pages", "api", "_app", "_document"];
|
|
462
|
+
for (const f of pageFiles) {
|
|
463
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
464
|
+
const appIdx = parts.indexOf("app");
|
|
465
|
+
const pagesIdx = parts.indexOf("pages");
|
|
466
|
+
const baseIdx = appIdx >= 0 ? appIdx : pagesIdx;
|
|
467
|
+
if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
|
|
468
|
+
const domain = parts[baseIdx + 1];
|
|
469
|
+
if (!skipNames.includes(domain) && !domain.startsWith("_") && !domain.startsWith("(") && !domain.startsWith("[") && !domain.startsWith(".")) {
|
|
470
|
+
if (!domainSet[domain]) domainSet[domain] = { pages: 0, clientFiles: 0, totalFiles: 0 };
|
|
471
|
+
domainSet[domain].pages++;
|
|
472
|
+
domainSet[domain].totalFiles++;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Count client.tsx as well
|
|
477
|
+
const clientFiles = await glob("**/client.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
|
|
478
|
+
for (const f of clientFiles) {
|
|
479
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
480
|
+
const appIdx = parts.indexOf("app");
|
|
481
|
+
const baseIdx = appIdx >= 0 ? appIdx : -1;
|
|
482
|
+
if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
|
|
483
|
+
const domain = parts[baseIdx + 1];
|
|
484
|
+
if (domainSet[domain]) {
|
|
485
|
+
domainSet[domain].clientFiles++;
|
|
486
|
+
domainSet[domain].totalFiles++;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
for (const [name, data] of Object.entries(domainSet)) {
|
|
491
|
+
frontendDomains.push({
|
|
492
|
+
name, type: "frontend", pages: data.pages, clientFiles: data.clientFiles, totalFiles: data.totalFiles,
|
|
493
|
+
rscPattern: data.clientFiles > 0 ? "RSC+Client split" : "default",
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Also scan widgets/features/entities directly (glob fallback)
|
|
498
|
+
for (const layer of ["widgets", "features", "entities"]) {
|
|
499
|
+
const layerFiles = await glob(`**/${layer}/*/**/*.{tsx,jsx,ts,js}`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/*.spec.*", "**/*.test.*"] });
|
|
500
|
+
const layerDomains = {};
|
|
501
|
+
for (const f of layerFiles) {
|
|
502
|
+
const parts = f.replace(/\\/g, "/").split("/");
|
|
503
|
+
const layerIdx = parts.indexOf(layer);
|
|
504
|
+
if (layerIdx >= 0 && layerIdx + 1 < parts.length) {
|
|
505
|
+
const domain = parts[layerIdx + 1];
|
|
506
|
+
if (!["ui", "common", "shared", "lib", "config"].includes(domain)) {
|
|
507
|
+
if (!layerDomains[domain]) layerDomains[domain] = 0;
|
|
508
|
+
layerDomains[domain]++;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
for (const [name, count] of Object.entries(layerDomains)) {
|
|
513
|
+
frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", totalFiles: count });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ── Python/Django ──
|
|
520
|
+
if (stack.framework === "django") {
|
|
521
|
+
const candidates = await glob("**/models.py", { cwd: ROOT, ignore: ["**/node_modules/**", "**/venv/**", "**/.venv/**", "**/env/**", "**/migrations/**"] });
|
|
522
|
+
for (const f of candidates) {
|
|
523
|
+
const dir = path.dirname(f);
|
|
524
|
+
if (dir === "." || dir.includes("venv")) continue;
|
|
525
|
+
const name = path.basename(dir);
|
|
526
|
+
const appFiles = await glob(`${dir}/*.py`, { cwd: ROOT });
|
|
527
|
+
const views = appFiles.filter(x => x.includes("views")).length;
|
|
528
|
+
const models = appFiles.filter(x => x.includes("models")).length;
|
|
529
|
+
const serializers = appFiles.filter(x => x.includes("serializers")).length;
|
|
530
|
+
backendDomains.push({ name, type: "backend", views, models, serializers, totalFiles: appFiles.length });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Python/FastAPI ──
|
|
535
|
+
if (stack.framework === "fastapi" || stack.framework === "flask") {
|
|
536
|
+
const routerFiles = await glob("**/{router,routes,endpoints}*.py", { cwd: ROOT, ignore: ["**/venv/**", "**/.venv/**"] });
|
|
537
|
+
const seen = new Set();
|
|
538
|
+
for (const f of routerFiles) {
|
|
539
|
+
const dir = path.dirname(f);
|
|
540
|
+
const name = path.basename(dir);
|
|
541
|
+
if (seen.has(name) || ["venv", ".venv", "__pycache__"].includes(name)) continue;
|
|
542
|
+
seen.add(name);
|
|
543
|
+
const appFiles = await glob(`${dir}/*.py`, { cwd: ROOT });
|
|
544
|
+
backendDomains.push({ name, type: "backend", totalFiles: appFiles.length });
|
|
545
|
+
}
|
|
546
|
+
if (backendDomains.filter(d => d.type === "backend").length === 0) {
|
|
547
|
+
const appDirs = await glob("{app,src/app}/*/", { cwd: ROOT });
|
|
548
|
+
for (let dir of appDirs) {
|
|
549
|
+
if (!dir.endsWith("/")) dir += "/";
|
|
550
|
+
const name = path.basename(dir.replace(/\/$/, ""));
|
|
551
|
+
if (["core", "common", "utils", "__pycache__"].includes(name)) continue;
|
|
552
|
+
const files = await glob(`${dir.replace(/\\/g, "/")}*.py`, { cwd: ROOT });
|
|
553
|
+
if (files.length > 0) backendDomains.push({ name, type: "backend", totalFiles: files.length });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Frontend count
|
|
559
|
+
const frontend = { exists: false, components: 0, pages: 0, hooks: 0 };
|
|
560
|
+
if (stack.frontend) {
|
|
561
|
+
frontend.exists = true;
|
|
562
|
+
frontend.components = (await glob("{src/,}**/components/**/*.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
563
|
+
frontend.pages = (await glob("{src/,}{app,pages}/**/{page,index}.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
564
|
+
frontend.hooks = (await glob("{src/,}**/hooks/**/*.{ts,js}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// App Router RSC/Client overall stats (for project-analysis.json)
|
|
568
|
+
if (stack.frontend === "nextjs") {
|
|
569
|
+
const allClientFiles = await glob("{app,src/app}/**/client.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
570
|
+
const allPageFiles = await glob("{app,src/app}/**/page.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
571
|
+
const allLayoutFiles = await glob("{app,src/app}/**/layout.{tsx,ts,jsx,js}", { cwd: ROOT });
|
|
572
|
+
frontend.clientComponents = allClientFiles.length;
|
|
573
|
+
frontend.serverPages = allPageFiles.length;
|
|
574
|
+
frontend.layouts = allLayoutFiles.length;
|
|
575
|
+
frontend.rscPattern = allClientFiles.length > 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// All domains = backend + frontend (with type tags)
|
|
579
|
+
const allDomains = [
|
|
580
|
+
...backendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
581
|
+
...frontendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
return { domains: allDomains, backendDomains, frontendDomains, rootPackage, frontend };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ─── Resolve shared query modules ────────────────────────────────
|
|
588
|
+
// When a shared query module (e.g., common-query-server) contains controllers/services
|
|
589
|
+
// for multiple domains, extract the actual domains and merge them into existing entries.
|
|
590
|
+
// Supports two extraction patterns:
|
|
591
|
+
// A) Package-based: .../kotlin/com/company/.../DOMAIN/controller/XxxController.kt
|
|
592
|
+
// B) Class-name-based: .../controller/ReservationQueryController.kt → "reservation"
|
|
593
|
+
// Safety: if no shared modules are found, this function does nothing (no-op).
|
|
594
|
+
|
|
595
|
+
function resolveSharedQueryDomains(backendDomains, ktFiles) {
|
|
596
|
+
const genericNames = ["common", "shared", "base", "core", "global"];
|
|
597
|
+
const layerNames = new Set([
|
|
598
|
+
"controller", "service", "repository", "mapper", "api", "handler",
|
|
599
|
+
"dto", "model", "entity", "config", "configuration", "util", "utils",
|
|
600
|
+
"infra", "infrastructure", "framework", "common", "shared", "base",
|
|
601
|
+
"core", "global", "internal", "external", "exception", "interceptor",
|
|
602
|
+
"filter", "converter", "event", "listener", "client", "feign",
|
|
603
|
+
"adapter", "port", "domain", "application", "presentation",
|
|
604
|
+
"persistence", "web", "rest", "grpc", "query", "command",
|
|
605
|
+
"aggregate", "aggregator", "vo", "valueobject",
|
|
606
|
+
]);
|
|
607
|
+
const skipPackages = new Set([
|
|
608
|
+
"com", "org", "net", "io", "kr", "jp", "cn", "de", "fr", "uk", "us",
|
|
609
|
+
"dev", "app", "main", "server", "backend", "project", "kotlin",
|
|
610
|
+
]);
|
|
611
|
+
const classSuffixes = new Set([
|
|
612
|
+
"Controller", "Service", "Repository", "Mapper", "Handler", "Api",
|
|
613
|
+
"Query", "Command", "Read", "Write", "Get", "Find", "List", "Search",
|
|
614
|
+
"Admin", "Dto", "Request", "Response", "Entity", "Model", "Impl",
|
|
615
|
+
"Factory", "Builder", "Adapter", "Client", "Facade", "Provider",
|
|
616
|
+
]);
|
|
617
|
+
|
|
618
|
+
// Find shared modules: generic name + query server type
|
|
619
|
+
const sharedModules = backendDomains.filter(d =>
|
|
620
|
+
d.pattern === "kotlin-multimodule" &&
|
|
621
|
+
d.serverTypes && d.serverTypes.includes("query") &&
|
|
622
|
+
genericNames.some(g => d.name.startsWith(g))
|
|
623
|
+
);
|
|
624
|
+
if (sharedModules.length === 0) return;
|
|
625
|
+
|
|
626
|
+
// Existing domain names for Pattern B-1 matching (longest-first for greedy match)
|
|
627
|
+
const existingDomains = backendDomains
|
|
628
|
+
.filter(d => !sharedModules.includes(d) && d.pattern === "kotlin-multimodule")
|
|
629
|
+
.map(d => d.name)
|
|
630
|
+
.sort((a, b) => b.length - a.length);
|
|
631
|
+
|
|
632
|
+
for (const shared of sharedModules) {
|
|
633
|
+
const moduleNames = shared.modules || [];
|
|
634
|
+
const sharedKtFiles = ktFiles.filter(f =>
|
|
635
|
+
moduleNames.some(m => f.includes(`/${m}/`) || f.startsWith(`${m}/`))
|
|
636
|
+
);
|
|
637
|
+
if (sharedKtFiles.length === 0) continue;
|
|
638
|
+
|
|
639
|
+
const domainFileMap = {}; // { domainName: [filePaths] }
|
|
640
|
+
|
|
641
|
+
for (const filePath of sharedKtFiles) {
|
|
642
|
+
let domain = null;
|
|
643
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
644
|
+
|
|
645
|
+
// ── Pattern A: Package-based ──
|
|
646
|
+
// Looks for: .../kotlin/com/company/[project/]DOMAIN/LAYER/File.kt
|
|
647
|
+
// Uses depth >= 3 from kotlin/ to skip base package segments
|
|
648
|
+
const kotlinIdx = parts.findIndex(p => p === "kotlin");
|
|
649
|
+
if (kotlinIdx >= 0) {
|
|
650
|
+
for (let i = kotlinIdx + 1; i < parts.length - 1; i++) {
|
|
651
|
+
if (layerNames.has(parts[i].toLowerCase())) {
|
|
652
|
+
if (i - 1 > kotlinIdx) {
|
|
653
|
+
const candidate = parts[i - 1].toLowerCase();
|
|
654
|
+
const depth = (i - 1) - kotlinIdx; // distance from kotlin/
|
|
655
|
+
if (
|
|
656
|
+
depth >= 3 &&
|
|
657
|
+
!layerNames.has(candidate) &&
|
|
658
|
+
!skipPackages.has(candidate) &&
|
|
659
|
+
candidate.length > 2
|
|
660
|
+
) {
|
|
661
|
+
domain = candidate;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
break; // only check first layer directory encountered
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ── Pattern B-1: Match class name against existing domain names ──
|
|
670
|
+
if (!domain) {
|
|
671
|
+
const fileName = parts[parts.length - 1].replace(/\.kt$/, "").toLowerCase();
|
|
672
|
+
for (const ed of existingDomains) {
|
|
673
|
+
const normalized = ed.replace(/-/g, "");
|
|
674
|
+
if (fileName.startsWith(normalized) && fileName.length > normalized.length) {
|
|
675
|
+
domain = ed;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ── Pattern B-2: Extract from PascalCase class name ──
|
|
682
|
+
if (!domain) {
|
|
683
|
+
const fileName = parts[parts.length - 1].replace(/\.kt$/, "");
|
|
684
|
+
const words = fileName.match(/[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$)/g);
|
|
685
|
+
if (words && words.length >= 2) {
|
|
686
|
+
const domainWords = [];
|
|
687
|
+
for (const w of words) {
|
|
688
|
+
if (classSuffixes.has(w)) break;
|
|
689
|
+
domainWords.push(w);
|
|
690
|
+
}
|
|
691
|
+
if (domainWords.length > 0) {
|
|
692
|
+
const extracted = domainWords.join("").toLowerCase();
|
|
693
|
+
if (
|
|
694
|
+
!genericNames.includes(extracted) &&
|
|
695
|
+
!skipPackages.has(extracted) &&
|
|
696
|
+
extracted.length > 2
|
|
697
|
+
) {
|
|
698
|
+
domain = extracted;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (domain) {
|
|
705
|
+
if (!domainFileMap[domain]) domainFileMap[domain] = [];
|
|
706
|
+
domainFileMap[domain].push(filePath);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ── Merge extracted domains into backendDomains ──
|
|
711
|
+
let distributedFiles = 0;
|
|
712
|
+
for (const [domain, files] of Object.entries(domainFileMap)) {
|
|
713
|
+
// Find matching existing domain (exact or normalized)
|
|
714
|
+
const existing = backendDomains.find(d =>
|
|
715
|
+
d !== shared &&
|
|
716
|
+
(d.name === domain || d.name.replace(/-/g, "") === domain.replace(/-/g, ""))
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
const ctrlCount = files.filter(f =>
|
|
720
|
+
/\/controller\//i.test(f) || /Controller\.kt$/i.test(f)
|
|
721
|
+
).length;
|
|
722
|
+
const svcCount = files.filter(f =>
|
|
723
|
+
/\/service\//i.test(f) || /Service\.kt$/i.test(f)
|
|
724
|
+
).length;
|
|
725
|
+
const repoCount = files.filter(f =>
|
|
726
|
+
/\/repository\//i.test(f) || /\/mapper\//i.test(f) ||
|
|
727
|
+
/Repository\.kt$/i.test(f) || /Mapper\.kt$/i.test(f)
|
|
728
|
+
).length;
|
|
729
|
+
const dtoCount = files.filter(f =>
|
|
730
|
+
(/\/dto\//i.test(f) || /\/vo\//i.test(f) || /Dto\.kt$/i.test(f) ||
|
|
731
|
+
/Vo\.kt$/i.test(f) || /Request\.kt$/i.test(f) || /Response\.kt$/i.test(f)) &&
|
|
732
|
+
!/\/controller\//i.test(f) && !/\/service\//i.test(f) &&
|
|
733
|
+
!/\/repository\//i.test(f)
|
|
734
|
+
).length;
|
|
735
|
+
|
|
736
|
+
if (existing) {
|
|
737
|
+
// Merge into existing domain
|
|
738
|
+
if (!existing.modules) existing.modules = [];
|
|
739
|
+
for (const m of moduleNames) {
|
|
740
|
+
if (!existing.modules.includes(m)) existing.modules.push(m);
|
|
741
|
+
}
|
|
742
|
+
if (!existing.serverTypes) existing.serverTypes = [];
|
|
743
|
+
if (!existing.serverTypes.includes("query")) existing.serverTypes.push("query");
|
|
744
|
+
existing.controllers += ctrlCount;
|
|
745
|
+
existing.services += svcCount;
|
|
746
|
+
if (existing.mappers != null) existing.mappers += repoCount;
|
|
747
|
+
existing.dtos += dtoCount;
|
|
748
|
+
existing.totalFiles += files.length;
|
|
749
|
+
} else {
|
|
750
|
+
// Create new domain entry
|
|
751
|
+
backendDomains.push({
|
|
752
|
+
name: domain,
|
|
753
|
+
type: "backend",
|
|
754
|
+
controllers: ctrlCount,
|
|
755
|
+
services: svcCount,
|
|
756
|
+
mappers: repoCount,
|
|
757
|
+
dtos: dtoCount,
|
|
758
|
+
totalFiles: files.length,
|
|
759
|
+
modules: [...moduleNames],
|
|
760
|
+
serverTypes: ["query"],
|
|
761
|
+
pattern: "kotlin-multimodule",
|
|
762
|
+
resolvedFrom: shared.name,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
distributedFiles += files.length;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Adjust shared module: keep only undistributed files
|
|
769
|
+
if (distributedFiles > 0) {
|
|
770
|
+
shared.totalFiles = Math.max(0, shared.totalFiles - distributedFiles);
|
|
771
|
+
shared.resolvedDomains = Object.keys(domainFileMap);
|
|
772
|
+
if (shared.totalFiles === 0) shared._resolved = true;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Remove fully resolved shared entries (all files distributed)
|
|
777
|
+
for (let i = backendDomains.length - 1; i >= 0; i--) {
|
|
778
|
+
if (backendDomains[i]._resolved) backendDomains.splice(i, 1);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
module.exports = { scanStructure, resolveSharedQueryDomains };
|