claudeos-core 1.0.7 → 1.2.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.

Potentially problematic release.


This version of claudeos-core might be problematic. Click here for more details.

Files changed (44) hide show
  1. package/CHANGELOG.md +84 -1
  2. package/CONTRIBUTING.md +15 -4
  3. package/README.de.md +187 -11
  4. package/README.es.md +187 -11
  5. package/README.fr.md +187 -11
  6. package/README.hi.md +187 -11
  7. package/README.ja.md +186 -10
  8. package/README.ko.md +331 -364
  9. package/README.md +200 -11
  10. package/README.ru.md +187 -11
  11. package/README.vi.md +188 -12
  12. package/README.zh-CN.md +186 -10
  13. package/bin/cli.js +183 -61
  14. package/bootstrap.sh +128 -21
  15. package/content-validator/index.js +131 -60
  16. package/health-checker/index.js +29 -23
  17. package/import-linter/index.js +14 -8
  18. package/manifest-generator/index.js +26 -20
  19. package/package.json +84 -75
  20. package/pass-json-validator/index.js +92 -70
  21. package/pass-prompts/templates/common/header.md +4 -4
  22. package/pass-prompts/templates/common/lang-instructions.json +27 -0
  23. package/pass-prompts/templates/common/pass3-footer.md +2 -3
  24. package/pass-prompts/templates/java-spring/pass1.md +84 -81
  25. package/pass-prompts/templates/java-spring/pass2.md +66 -66
  26. package/pass-prompts/templates/java-spring/pass3.md +60 -60
  27. package/pass-prompts/templates/kotlin-spring/pass1.md +172 -0
  28. package/pass-prompts/templates/kotlin-spring/pass2.md +109 -0
  29. package/pass-prompts/templates/kotlin-spring/pass3.md +98 -0
  30. package/pass-prompts/templates/node-express/pass1.md +73 -73
  31. package/pass-prompts/templates/node-express/pass2.md +66 -66
  32. package/pass-prompts/templates/node-express/pass3.md +53 -53
  33. package/pass-prompts/templates/node-nextjs/pass1.md +68 -68
  34. package/pass-prompts/templates/node-nextjs/pass2.md +61 -61
  35. package/pass-prompts/templates/node-nextjs/pass3.md +48 -48
  36. package/pass-prompts/templates/python-django/pass1.md +78 -78
  37. package/pass-prompts/templates/python-django/pass2.md +69 -69
  38. package/pass-prompts/templates/python-django/pass3.md +45 -45
  39. package/pass-prompts/templates/python-fastapi/pass1.md +76 -76
  40. package/pass-prompts/templates/python-fastapi/pass2.md +67 -67
  41. package/pass-prompts/templates/python-fastapi/pass3.md +45 -45
  42. package/plan-installer/index.js +623 -97
  43. package/plan-validator/index.js +54 -23
  44. package/sync-checker/index.js +25 -14
@@ -3,17 +3,17 @@
3
3
  /**
4
4
  * ClaudeOS-Core — plan-installer
5
5
  *
6
- * 역할: 프로젝트 분석스택 감지도메인 목록그룹 자동 분할프롬프트 동적 생성
7
- * 출력:
8
- * - claudeos-core/generated/project-analysis.json (스택/구조 정보, 멀티스택 포함)
9
- * - claudeos-core/generated/domain-groups.json (Pass 1 실행용 그룹 분할, 타입별)
10
- * - claudeos-core/generated/pass1-backend-prompt.md (백엔드 분석 프롬프트)
11
- * - claudeos-core/generated/pass1-frontend-prompt.md (프론트엔드 분석 프롬프트, 감지 )
12
- * - claudeos-core/generated/pass1-prompt.md (단일스택 하위 호환)
6
+ * Role: Project analysisStack detectionDomain listAuto group splittingDynamic prompt generation
7
+ * Output:
8
+ * - claudeos-core/generated/project-analysis.json (stack/structure info, multi-stack aware)
9
+ * - claudeos-core/generated/domain-groups.json (group splitting for Pass 1 execution, per type)
10
+ * - claudeos-core/generated/pass1-backend-prompt.md (backend analysis prompt)
11
+ * - claudeos-core/generated/pass1-frontend-prompt.md (frontend analysis prompt, if detected)
12
+ * - claudeos-core/generated/pass1-prompt.md (single-stack backward compat)
13
13
  * - claudeos-core/generated/pass2-prompt.md
14
- * - claudeos-core/generated/pass3-prompt.md (멀티스택 결합 프롬프트)
14
+ * - claudeos-core/generated/pass3-prompt.md (combined prompt for multi-stack)
15
15
  *
16
- * 실행: npx claudeos-core <cmd> 또는 node claudeos-core-tools/plan-installer/index.js
16
+ * Usage: npx claudeos-core <cmd> or node claudeos-core-tools/plan-installer/index.js
17
17
  */
18
18
 
19
19
  const fs = require("fs");
@@ -28,7 +28,7 @@ function ensureDir(dir) {
28
28
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
29
29
  }
30
30
 
31
- // ─── 스택 감지 (멀티스택) ────────────────────────────────
31
+ // ─── Stack detection (multi-stack) ────────────────────────────────
32
32
  async function detectStack() {
33
33
  const stack = {
34
34
  language: null, languageVersion: null,
@@ -38,20 +38,137 @@ async function detectStack() {
38
38
  packageManager: null, detected: [],
39
39
  };
40
40
 
41
- // ── Java: Gradle ──
42
- if (fs.existsSync(path.join(ROOT, "build.gradle"))) {
43
- const g = fs.readFileSync(path.join(ROOT, "build.gradle"), "utf-8");
44
- stack.buildTool = "gradle"; stack.detected.push("build.gradle");
41
+ // ── Java/Kotlin: Gradle ──
42
+ const gradleFile = fs.existsSync(path.join(ROOT, "build.gradle.kts"))
43
+ ? "build.gradle.kts"
44
+ : fs.existsSync(path.join(ROOT, "build.gradle")) ? "build.gradle" : null;
45
+ if (gradleFile) {
46
+ const g = fs.readFileSync(path.join(ROOT, gradleFile), "utf-8");
47
+ stack.buildTool = "gradle"; stack.detected.push(gradleFile);
45
48
  if (g.includes("spring-boot")) { stack.language = "java"; stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
46
- const sv = g.match(/org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/);
47
- if (sv) stack.frameworkVersion = sv[1];
49
+ const svPatterns = [
50
+ /org\.springframework\.boot.*version\s*['"]([^'"]+)['"]/,
51
+ /id\s*\(\s*["']org\.springframework\.boot["']\s*\)\s*version\s*["']([^"']+)["']/,
52
+ /spring-boot-dependencies:([^'")\s]+)/,
53
+ ];
54
+ for (const pattern of svPatterns) {
55
+ const sv = g.match(pattern);
56
+ if (sv) { stack.frameworkVersion = sv[1]; break; }
57
+ }
48
58
  const jv = g.match(/sourceCompatibility\s*=\s*['"]?(\d+)['"]?/);
49
59
  if (jv) stack.languageVersion = jv[1];
50
- if (g.includes("mybatis")) { stack.orm = "mybatis"; stack.detected.push("mybatis"); }
51
- if (g.includes("jpa") || g.includes("hibernate")) { stack.orm = "jpa"; stack.detected.push("jpa"); }
52
- if (g.includes("postgresql")) { stack.database = "postgresql"; stack.detected.push("postgresql"); }
53
- if (g.includes("mysql")) { stack.database = "mysql"; stack.detected.push("mysql"); }
54
- if (g.includes("oracle")) { stack.database = "oracle"; stack.detected.push("oracle"); }
60
+ // ORM detection (first match wins order by specificity)
61
+ if (!stack.orm && g.includes("mybatis")) { stack.orm = "mybatis"; stack.detected.push("mybatis"); }
62
+ if (!stack.orm && (g.includes("jpa") || g.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa"); }
63
+ if (!stack.orm && g.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed"); }
64
+ if (!stack.orm && g.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq"); }
65
+ if (!stack.orm && g.includes("spring-data-jdbc")) { stack.orm = "spring-data-jdbc"; stack.detected.push("spring-data-jdbc"); }
66
+ if (!stack.orm && g.includes("r2dbc")) { stack.orm = "r2dbc"; stack.detected.push("r2dbc"); }
67
+ // DB detection
68
+ if (!stack.database && g.includes("postgresql")) { stack.database = "postgresql"; stack.detected.push("postgresql"); }
69
+ if (!stack.database && g.includes("mysql")) { stack.database = "mysql"; stack.detected.push("mysql"); }
70
+ if (!stack.database && g.includes("oracle")) { stack.database = "oracle"; stack.detected.push("oracle"); }
71
+ if (!stack.database && g.includes("mongodb")) { stack.database = "mongodb"; stack.detected.push("mongodb"); }
72
+ if (!stack.database && g.includes("h2")) { stack.database = "h2"; stack.detected.push("h2"); }
73
+ // Kotlin detection: override language if Kotlin plugin found
74
+ if (g.includes("kotlin") || g.includes("org.jetbrains.kotlin")) {
75
+ stack.language = "kotlin"; stack.detected.push("kotlin");
76
+ // Try multiple patterns for Kotlin version extraction
77
+ const kvPatterns = [
78
+ /kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
79
+ /org\.jetbrains\.kotlin\S*\s*version\s*['"]([^'"]+)['"]/,
80
+ /kotlin\("jvm"\)\s*version\s*["']([^"']+)["']/,
81
+ ];
82
+ for (const pattern of kvPatterns) {
83
+ const match = g.match(pattern);
84
+ if (match) { stack.languageVersion = match[1]; break; }
85
+ }
86
+ }
87
+ }
88
+
89
+ // ── Gradle: version catalogs (libs.versions.toml) — fallback for versions & deps ──
90
+ const versionCatalog = path.join(ROOT, "gradle/libs.versions.toml");
91
+ if (fs.existsSync(versionCatalog)) {
92
+ const vc = fs.readFileSync(versionCatalog, "utf-8");
93
+ stack.detected.push("libs.versions.toml");
94
+ // Language version fallback
95
+ if (!stack.languageVersion) {
96
+ const kvMatch = vc.match(/kotlin\s*=\s*["']([^"']+)["']/);
97
+ if (kvMatch) stack.languageVersion = kvMatch[1];
98
+ }
99
+ if (!stack.frameworkVersion) {
100
+ const sbMatch = vc.match(/spring-boot\s*=\s*["']([^"']+)["']/);
101
+ if (sbMatch) stack.frameworkVersion = sbMatch[1];
102
+ }
103
+ // ORM/DB detection from version catalog libraries
104
+ if (!stack.orm && vc.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed (catalog)"); }
105
+ if (!stack.orm && vc.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq (catalog)"); }
106
+ if (!stack.orm && (vc.includes("jpa") || vc.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa (catalog)"); }
107
+ if (!stack.database && vc.includes("postgresql")) { stack.database = "postgresql"; }
108
+ if (!stack.database && vc.includes("mysql")) { stack.database = "mysql"; }
109
+ if (!stack.database && vc.includes("mongodb")) { stack.database = "mongodb"; }
110
+ // Kotlin detection from catalog
111
+ if (!stack.language && vc.includes("kotlin")) {
112
+ stack.language = "kotlin"; stack.detected.push("kotlin (catalog)");
113
+ }
114
+ }
115
+
116
+ // ── Kotlin: multi-module Gradle detection (check subproject build files) ──
117
+ if (stack.language !== "kotlin" && stack.buildTool === "gradle") {
118
+ const subBuildFiles = await glob("**/build.gradle{,.kts}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**"] });
119
+ for (const sbf of subBuildFiles.slice(0, 5)) {
120
+ const sc = fs.readFileSync(path.join(ROOT, sbf), "utf-8");
121
+ if (sc.includes("kotlin") || sc.includes("org.jetbrains.kotlin")) {
122
+ stack.language = "kotlin"; stack.detected.push("kotlin (submodule)");
123
+ const kv = sc.match(/kotlin\S*\s*version\s*['"]([^'"]+)['"]/);
124
+ if (kv) stack.languageVersion = kv[1];
125
+ break;
126
+ }
127
+ }
128
+ }
129
+
130
+ // ── Kotlin: detect CQRS/multi-module architecture from settings.gradle ──
131
+ const settingsFile = fs.existsSync(path.join(ROOT, "settings.gradle.kts"))
132
+ ? "settings.gradle.kts"
133
+ : fs.existsSync(path.join(ROOT, "settings.gradle")) ? "settings.gradle" : null;
134
+ if (settingsFile && stack.language === "kotlin") {
135
+ const sg = fs.readFileSync(path.join(ROOT, settingsFile), "utf-8");
136
+
137
+ // Extract all module names from include() calls
138
+ // Handles: include(":m1", ":m2"), include(":m1"), include ":m1", ":m2"
139
+ // Also strips leading ":" prefix (Gradle subproject path convention)
140
+ // Pre-filter: remove single-line comments to avoid matching commented-out includes
141
+ const sgClean = sg.split("\n").filter(l => !l.trimStart().startsWith("//")).join("\n");
142
+ const includes = [];
143
+ // Kotlin DSL: include(":m1", ":m2", ":m3") — may span multiple lines
144
+ const includeBlocks = [...sgClean.matchAll(/include\s*\(([^)]*)\)/gs)];
145
+ for (const block of includeBlocks) {
146
+ const quotedValues = [...block[1].matchAll(/["']([^"']+)["']/g)].map(m => m[1]);
147
+ includes.push(...quotedValues);
148
+ }
149
+ // Groovy DSL: include ':m1', ':m2', ':m3' — single line, comma-separated
150
+ if (includes.length === 0) {
151
+ const groovyIncludes = [...sgClean.matchAll(/include\s+(.+)/g)];
152
+ for (const line of groovyIncludes) {
153
+ const quotedValues = [...line[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
154
+ includes.push(...quotedValues);
155
+ }
156
+ }
157
+
158
+ // Strip leading ":" prefix from all module names (Gradle convention)
159
+ const cleanModules = includes.map(m => m.replace(/^:/, ""));
160
+
161
+ if (cleanModules.length > 0) {
162
+ stack.multiModule = true;
163
+ stack.modules = cleanModules;
164
+ stack.detected.push(`multi-module (${cleanModules.length} modules)`);
165
+ // Detect CQRS from module names
166
+ const hasCommand = cleanModules.some(m => m.includes("command"));
167
+ const hasQuery = cleanModules.some(m => m.includes("query"));
168
+ const hasBff = cleanModules.some(m => m.includes("bff"));
169
+ if (hasCommand && hasQuery) { stack.architecture = "cqrs"; stack.detected.push("cqrs"); }
170
+ if (hasBff) { stack.detected.push("bff"); }
171
+ }
55
172
  }
56
173
 
57
174
  // ── Java: Maven ──
@@ -63,28 +180,36 @@ async function detectStack() {
63
180
  const jv = pom.match(/<java\.version>(\d+)/);
64
181
  if (jv) stack.languageVersion = jv[1];
65
182
  if (pom.includes("spring-boot") && !stack.framework) { stack.framework = "spring-boot"; stack.detected.push("spring-boot"); }
66
- if (pom.includes("mybatis") && !stack.orm) { stack.orm = "mybatis"; stack.detected.push("mybatis"); }
67
- if (pom.includes("jpa") || pom.includes("hibernate")) { if (!stack.orm) stack.orm = "jpa"; stack.detected.push("jpa"); }
68
- if (pom.includes("postgresql") && !stack.database) stack.database = "postgresql";
69
- if (pom.includes("mysql") && !stack.database) stack.database = "mysql";
70
- if (pom.includes("oracle") && !stack.database) stack.database = "oracle";
183
+ if (!stack.orm && pom.includes("mybatis")) { stack.orm = "mybatis"; stack.detected.push("mybatis"); }
184
+ if (!stack.orm && (pom.includes("jpa") || pom.includes("hibernate"))) { stack.orm = "jpa"; stack.detected.push("jpa"); }
185
+ if (!stack.orm && pom.includes("exposed")) { stack.orm = "exposed"; stack.detected.push("exposed"); }
186
+ if (!stack.orm && pom.includes("jooq")) { stack.orm = "jooq"; stack.detected.push("jooq"); }
187
+ if (!stack.database && pom.includes("postgresql")) stack.database = "postgresql";
188
+ if (!stack.database && pom.includes("mysql")) stack.database = "mysql";
189
+ if (!stack.database && pom.includes("oracle")) stack.database = "oracle";
190
+ if (!stack.database && pom.includes("mongodb")) stack.database = "mongodb";
191
+ if (!stack.database && pom.includes("h2")) stack.database = "h2";
192
+ if (!stack.database && pom.includes("sqlite")) stack.database = "sqlite";
71
193
  }
72
194
 
73
195
  // ── Node.js ──
74
196
  if (fs.existsSync(path.join(ROOT, "package.json"))) {
75
- const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8"));
197
+ let pkg;
198
+ try { pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf-8")); }
199
+ catch { console.warn(" ⚠️ package.json parse error — skipping Node.js detection"); pkg = null; }
200
+ if (!pkg) return stack;
76
201
  stack.detected.push("package.json");
77
202
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
78
203
 
79
204
  if (!stack.language) stack.language = deps.typescript ? "typescript" : "javascript";
80
205
  if (deps.typescript) { stack.detected.push("typescript"); stack.languageVersion = deps.typescript.replace(/[^0-9.]/g, ""); }
81
206
 
82
- // 프론트엔드
207
+ // Frontend
83
208
  if (deps.next) { stack.frontend = "nextjs"; stack.detected.push("next.js"); stack.frontendVersion = deps.next.replace(/[^0-9.]/g, ""); }
84
209
  else if (deps.react) { stack.frontend = "react"; stack.detected.push("react"); stack.frontendVersion = deps.react.replace(/[^0-9.]/g, ""); }
85
210
  else if (deps.vue) { stack.frontend = "vue"; stack.detected.push("vue"); stack.frontendVersion = deps.vue.replace(/[^0-9.]/g, ""); }
86
211
 
87
- // 백엔드 프레임워크
212
+ // Backend framework
88
213
  if (deps.express && !stack.framework) { stack.framework = "express"; stack.detected.push("express"); }
89
214
  if (deps["@nestjs/core"]) { stack.framework = "nestjs"; stack.detected.push("nestjs"); stack.frameworkVersion = deps["@nestjs/core"].replace(/[^0-9.]/g, ""); }
90
215
 
@@ -101,7 +226,7 @@ async function detectStack() {
101
226
  if (deps.mysql2 && !stack.database) stack.database = "mysql";
102
227
  if (deps.mongodb && !stack.database) stack.database = "mongodb";
103
228
 
104
- // 패키지 매니저
229
+ // Package manager
105
230
  stack.packageManager = fs.existsSync(path.join(ROOT, "pnpm-lock.yaml")) ? "pnpm"
106
231
  : fs.existsSync(path.join(ROOT, "yarn.lock")) ? "yarn" : "npm";
107
232
 
@@ -149,11 +274,16 @@ async function detectStack() {
149
274
  const ymls = await glob("**/application*.yml", { cwd: ROOT, absolute: true, ignore: ["**/node_modules/**"] });
150
275
  for (const y of ymls) {
151
276
  const c = fs.readFileSync(y, "utf-8");
152
- if (c.includes("postgresql") && !stack.database) stack.database = "postgresql";
153
- if (c.includes("mysql") && !stack.database) stack.database = "mysql";
154
- if (c.includes("oracle") && !stack.database) stack.database = "oracle";
155
- const pm = c.match(/port:\s*(\d+)/);
156
- if (pm) stack.port = parseInt(pm[1]);
277
+ if (!stack.database && c.includes("postgresql")) stack.database = "postgresql";
278
+ if (!stack.database && c.includes("mysql")) stack.database = "mysql";
279
+ if (!stack.database && c.includes("oracle")) stack.database = "oracle";
280
+ if (!stack.database && c.includes("mongodb")) stack.database = "mongodb";
281
+ if (!stack.database && /\bh2\b/.test(c)) stack.database = "h2";
282
+ if (!stack.database && c.includes("sqlite")) stack.database = "sqlite";
283
+ if (!stack.port) {
284
+ const pm = c.match(/server:\s*\n\s*port:\s*(\d+)/) || c.match(/server\.port\s*[=:]\s*(\d+)/);
285
+ if (pm) stack.port = parseInt(pm[1]);
286
+ }
157
287
  }
158
288
 
159
289
  // .env
@@ -181,7 +311,7 @@ async function detectStack() {
181
311
  }
182
312
  }
183
313
 
184
- // ── Config file fallback (모노레포: package.json에 없어도 설정 파일로 감지) ──
314
+ // ── Config file fallback (monorepo: detect from config files when not in package.json) ──
185
315
  if (!stack.frontend) {
186
316
  const nextConfigs = ["next.config.js", "next.config.mjs", "next.config.ts"];
187
317
  if (nextConfigs.some(c => fs.existsSync(path.join(ROOT, c)))) {
@@ -205,7 +335,7 @@ async function detectStack() {
205
335
  return stack;
206
336
  }
207
337
 
208
- // ─── 구조 스캔 (멀티스택) ────────────────────────────────
338
+ // ─── Structure scan (multi-stack) ────────────────────────────────
209
339
  async function scanStructure(stack) {
210
340
  const backendDomains = [];
211
341
  const frontendDomains = [];
@@ -221,7 +351,7 @@ async function scanStructure(stack) {
221
351
  const domainMap = {};
222
352
  let detectedPattern = null;
223
353
 
224
- // Pattern A: controller/{domain}/*.java (레이어 우선 controller 아래에 도메인)
354
+ // Pattern A: controller/{domain}/*.java (layer-firstdomain under controller)
225
355
  const controllersA = await glob("src/main/java/**/controller/*/*.java", { cwd: ROOT });
226
356
  for (const f of controllersA) {
227
357
  const m = f.match(/controller\/([^/]+)\//);
@@ -233,8 +363,8 @@ async function scanStructure(stack) {
233
363
  }
234
364
  if (Object.keys(domainMap).length > 0) detectedPattern = "A";
235
365
 
236
- // Pattern B/D: {domain}/controller/*.java (도메인 우선 도메인 아래에 controller)
237
- // D B 확장: {module}/{domain}/controller/ — 동일 도메인명 충돌 시 module/domain 형태로 전환
366
+ // Pattern B/D: {domain}/controller/*.java (domain-firstcontroller under domain)
367
+ // D extends B: {module}/{domain}/controller/ — auto-upgrade to module/domain on name conflict
238
368
  if (!detectedPattern) {
239
369
  const controllersB = await glob("src/main/java/**/*/controller/*.java", { cwd: ROOT });
240
370
  const domainPaths = {};
@@ -249,11 +379,11 @@ async function scanStructure(stack) {
249
379
  }
250
380
  }
251
381
 
252
- // 동일 도메인명이 여러 모듈에서 발견되면 module/domain 형태로 (Pattern D)
382
+ // If same domain name found in multiple modules, use module/domain form (Pattern D)
253
383
  for (const [d, entries] of Object.entries(domainPaths)) {
254
384
  const modules = [...new Set(entries.map(e => e.module).filter(Boolean))];
255
385
  if (modules.length > 1) {
256
- // Pattern D: 충돌 — module/domain 형태로 등록
386
+ // Pattern D: conflictregister as module/domain
257
387
  for (const entry of entries) {
258
388
  const fullName = entry.module ? `${entry.module}/${d}` : d;
259
389
  if (!domainMap[fullName]) domainMap[fullName] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "D", modulePath: entry.module, domainName: d };
@@ -267,7 +397,7 @@ async function scanStructure(stack) {
267
397
  if (Object.keys(domainMap).length > 0) detectedPattern = domainMap[Object.keys(domainMap)[0]].pattern;
268
398
  }
269
399
 
270
- // Pattern E: DDD/헥사고날 — {domain}/adapter/in/web/*.java 또는 {domain}/adapter/in/rest/*.java
400
+ // Pattern E: DDD/Hexagonal — {domain}/adapter/in/web/*.java or {domain}/adapter/in/rest/*.java
271
401
  if (!detectedPattern) {
272
402
  const controllersE = await glob("src/main/java/**/adapter/in/{web,rest}/*.java", { cwd: ROOT });
273
403
  for (const f of controllersE) {
@@ -281,7 +411,7 @@ async function scanStructure(stack) {
281
411
  if (Object.keys(domainMap).length > 0) detectedPattern = "E";
282
412
  }
283
413
 
284
- // Pattern C: 플랫 구조 — controller/*.java (도메인 디렉토리 없음, 클래스명에서 도메인 추출)
414
+ // Pattern C: Flat structure — controller/*.java (no domain directory, extract domain from class name)
285
415
  if (!detectedPattern) {
286
416
  const controllersC = await glob("src/main/java/**/controller/*.java", { cwd: ROOT });
287
417
  for (const f of controllersC) {
@@ -295,8 +425,8 @@ async function scanStructure(stack) {
295
425
  if (Object.keys(domainMap).length > 0) detectedPattern = "C";
296
426
  }
297
427
 
298
- // ── 보충 스캔: controller 없는 서비스 도메인 감지 ──
299
- // core/delivery 처럼 service/mapper 있고 controller가 없는 도메인 포착
428
+ // ── Supplementary scan: detect service-only domains without controllers ──
429
+ // Catch domains like core/delivery that have service/mapper but no controller
300
430
  if (detectedPattern === "B" || detectedPattern === "D" || !detectedPattern) {
301
431
  const serviceDirs = await glob("src/main/java/**/*/service/*.java", { cwd: ROOT });
302
432
  const mapperDirs = await glob("src/main/java/**/*/{mapper,repository}/*.java", { cwd: ROOT });
@@ -313,7 +443,7 @@ async function scanStructure(stack) {
313
443
  }
314
444
  }
315
445
 
316
- // 도메인의 service/mapper/dto/xml 파일 스캔
446
+ // Scan service/mapper/dto/xml files for each domain
317
447
  for (const d of Object.keys(domainMap)) {
318
448
  const p = domainMap[d].pattern;
319
449
  const dn = domainMap[d].domainName || d;
@@ -332,7 +462,7 @@ async function scanStructure(stack) {
332
462
  mprGlob = `src/main/java/**/${d}/adapter/out/{persistence,repository}/*.java`;
333
463
  dtoGlob = `src/main/java/**/${d}/**/{dto,command,query}/**/*.java`;
334
464
  } else {
335
- // Pattern C: 플랫도메인명으로 파일명 매칭
465
+ // Pattern C: Flatmatch domain name from file name
336
466
  const cap = d.charAt(0).toUpperCase() + d.slice(1);
337
467
  svcGlob = `src/main/java/**/service/${cap}*.java`;
338
468
  mprGlob = `src/main/java/**/{mapper,repository}/${cap}*.java`;
@@ -351,7 +481,7 @@ async function scanStructure(stack) {
351
481
  backendDomains.push({ name: d, type: "backend", ...domainMap[d], totalFiles: svc.length + mpr.length + dto.length + xml.length + domainMap[d].controllers });
352
482
  }
353
483
 
354
- // ── Java 폴백: 글로브가 0개일 전체 .java 파일에서 직접 도메인 추출 ──
484
+ // ── Java fallback: extract domains directly from all .java files when glob returns 0 ──
355
485
  if (backendDomains.length === 0) {
356
486
  const allJava = await glob("**/*.java", { cwd: ROOT, ignore: ["**/node_modules/**", "**/build/**", "**/target/**", "**/test/**", "**/generated/**"] });
357
487
  const javaDomains = {};
@@ -363,11 +493,11 @@ async function scanStructure(stack) {
363
493
  const parts = f.replace(/\\/g, "/").split("/");
364
494
  for (let i = 0; i < parts.length - 1; i++) {
365
495
  if (layerNames.includes(parts[i]) && i > 0) {
366
- // Pattern A 감지: .../{domain}/controller/... 또는 .../controller/{domain}/...
496
+ // Pattern A detection: .../{domain}/controller/... or .../controller/{domain}/...
367
497
  const prevDir = parts[i - 1];
368
498
  const nextDir = parts[i + 1];
369
499
 
370
- // {domain}/layer/ 패턴 (도메인이 레이어 앞에)
500
+ // {domain}/layer/ pattern (domain before layer)
371
501
  if (!skipNames.includes(prevDir) && !layerNames.includes(prevDir) && !prevDir.includes(".") && !versionPattern.test(prevDir)) {
372
502
  if (!javaDomains[prevDir]) javaDomains[prevDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "B" };
373
503
  if (parts[i] === "controller") javaDomains[prevDir].controllers++;
@@ -375,7 +505,7 @@ async function scanStructure(stack) {
375
505
  else if (["mapper", "repository", "dao"].includes(parts[i])) javaDomains[prevDir].mappers++;
376
506
  else if (["dto", "vo"].includes(parts[i])) javaDomains[prevDir].dtos++;
377
507
  }
378
- // layer/{domain}/ 패턴 (레이어가 도메인 앞에)
508
+ // layer/{domain}/ pattern (layer before domain)
379
509
  if (nextDir && !nextDir.endsWith(".java") && !skipNames.includes(nextDir) && !layerNames.includes(nextDir) && !versionPattern.test(nextDir)) {
380
510
  if (!javaDomains[nextDir]) javaDomains[nextDir] = { controllers: 0, services: 0, mappers: 0, dtos: 0, xmlMappers: 0, pattern: "A" };
381
511
  if (parts[i] === "controller") javaDomains[nextDir].controllers++;
@@ -397,7 +527,191 @@ async function scanStructure(stack) {
397
527
  }
398
528
  }
399
529
 
400
- // ── Node.js 백엔드 (Express/NestJS) 프론트엔드 존재 여부와 무관하게 스캔 ──
530
+ // ── Kotlin (multi-module monorepo + CQRS support) ──
531
+ if (stack.language === "kotlin") {
532
+ // Scan all .kt files across all submodules
533
+ const ktFiles = await glob("**/src/main/kotlin/**/*.kt", {
534
+ cwd: ROOT,
535
+ ignore: ["**/node_modules/**", "**/build/**", "**/test/**", "**/generated/**"],
536
+ });
537
+
538
+ // Detect root package from first controller/service file
539
+ for (const f of ktFiles) {
540
+ const m = f.match(/src\/main\/kotlin\/(.+?)\/(controller|service|mapper|dto|entity|repository|adapter)/);
541
+ if (m) { rootPackage = m[1].replace(/\//g, "."); break; }
542
+ }
543
+
544
+ // Detect modules from directory structure (servers/*/ or direct submodules)
545
+ const moduleGlobs = [
546
+ "servers/*/*/src/main/kotlin/", // servers/command/reservation-command-server/
547
+ "servers/*/src/main/kotlin/", // servers/iam-server/
548
+ "*/src/main/kotlin/", // shared-lib/ or integration-lib/
549
+ ];
550
+ const moduleSet = {};
551
+ for (const mg of moduleGlobs) {
552
+ const moduleDirs = await glob(mg, { cwd: ROOT });
553
+ for (const md of moduleDirs) {
554
+ const parts = md.replace(/\/src\/main\/kotlin\/$/, "").split("/");
555
+ const moduleName = parts[parts.length - 1]; // e.g., "reservation-command-server"
556
+ if (moduleSet[moduleName]) {
557
+ // Name conflict: use full relative path as key to avoid silent loss
558
+ const fullKey = md.replace(/\/src\/main\/kotlin\/$/, "").replace(/\//g, "__");
559
+ moduleSet[fullKey] = md;
560
+ } else {
561
+ moduleSet[moduleName] = md;
562
+ }
563
+ }
564
+ }
565
+
566
+ // Categorize modules and extract domains
567
+ const skipModules = ["build", "gradle", "buildSrc", ".gradle", "node_modules"];
568
+ const serverTypes = { command: "command", query: "query", bff: "bff", integration: "integration" };
569
+ const domainMap = {};
570
+
571
+ for (const [moduleName, modulePath] of Object.entries(moduleSet)) {
572
+ if (skipModules.some(s => moduleName.includes(s))) continue;
573
+
574
+ // Determine server type from module name
575
+ let serverType = "standalone";
576
+ for (const [key, val] of Object.entries(serverTypes)) {
577
+ if (moduleName.includes(key)) { serverType = val; break; }
578
+ }
579
+
580
+ // Extract domain name from module name
581
+ // e.g., "reservation-command-server" → "reservation"
582
+ // e.g., "pms-bff-server" → "pms"
583
+ // e.g., "iam-server" → "iam"
584
+ // e.g., "shared-lib" → "shared-lib"
585
+ // e.g., "common-query-server" → "common-query" (shared server, not a domain named "common")
586
+ let domainName = moduleName
587
+ .replace(/-(?:command|query|bff|integration|adapter)-server$/, "")
588
+ .replace(/-server$/, "")
589
+ .replace(/-lib$/, "-lib");
590
+
591
+ // Guard: if domain extraction resulted in a generic name that is likely a shared module
592
+ // (e.g., "common-query-server" → "common"), keep the full type qualifier
593
+ const genericNames = ["common", "shared", "base", "core", "global", "internal"];
594
+ if (genericNames.includes(domainName) && serverType !== "standalone") {
595
+ domainName = `${domainName}-${serverType}`;
596
+ }
597
+
598
+ // Scan files in this module (append "/" to prevent prefix overlap: e.g., reservation vs reservation-ext)
599
+ const modulePrefix = modulePath.replace(/\/$/, "").replace(/\/src\/main\/kotlin$/, "") + "/";
600
+ const moduleKtFiles = ktFiles.filter(f => f.startsWith(modulePrefix) || f === modulePrefix.slice(0, -1));
601
+ const controllers = moduleKtFiles.filter(f => /controller/i.test(f)).length;
602
+ const services = moduleKtFiles.filter(f => /service/i.test(f)).length;
603
+ const repositories = moduleKtFiles.filter(f => /repository|mapper/i.test(f)).length;
604
+ const dtos = moduleKtFiles.filter(f => /dto|vo|request|response|model/i.test(f) && !/repository|service|controller/i.test(f)).length;
605
+ const totalFiles = moduleKtFiles.length;
606
+
607
+ if (totalFiles === 0) continue;
608
+
609
+ const key = `${domainName}:${serverType}`;
610
+ domainMap[key] = {
611
+ name: domainName,
612
+ moduleName,
613
+ modulePath,
614
+ serverType,
615
+ controllers,
616
+ services,
617
+ repositories,
618
+ dtos,
619
+ totalFiles,
620
+ };
621
+ }
622
+
623
+ // Group by domain: merge command/query/bff of same domain
624
+ const domainGroups = {};
625
+ for (const [key, info] of Object.entries(domainMap)) {
626
+ const { name } = info;
627
+ if (!domainGroups[name]) {
628
+ domainGroups[name] = { name, type: "backend", modules: [], controllers: 0, services: 0, repositories: 0, dtos: 0, totalFiles: 0, serverTypes: [] };
629
+ }
630
+ const dg = domainGroups[name];
631
+ dg.modules.push(info.moduleName);
632
+ dg.controllers += info.controllers;
633
+ dg.services += info.services;
634
+ dg.repositories += info.repositories;
635
+ dg.dtos += info.dtos;
636
+ dg.totalFiles += info.totalFiles;
637
+ if (!dg.serverTypes.includes(info.serverType)) dg.serverTypes.push(info.serverType);
638
+ }
639
+
640
+ // Push to backendDomains
641
+ for (const dg of Object.values(domainGroups)) {
642
+ backendDomains.push({
643
+ name: dg.name,
644
+ type: "backend",
645
+ controllers: dg.controllers,
646
+ services: dg.services,
647
+ mappers: dg.repositories,
648
+ dtos: dg.dtos,
649
+ totalFiles: dg.totalFiles,
650
+ modules: dg.modules,
651
+ serverTypes: dg.serverTypes,
652
+ pattern: "kotlin-multimodule",
653
+ });
654
+ }
655
+
656
+ // Also scan shared libraries as special domains
657
+ const libDirs = await glob("{shared-lib,integration-lib,*-lib}/src/main/kotlin/", { cwd: ROOT });
658
+ for (const ld of libDirs) {
659
+ const libName = ld.split("/")[0];
660
+ if (domainGroups[libName]) continue; // Already captured
661
+ const libFiles = ktFiles.filter(f => f.startsWith(libName));
662
+ if (libFiles.length > 0) {
663
+ backendDomains.push({
664
+ name: libName,
665
+ type: "backend",
666
+ controllers: 0,
667
+ services: libFiles.filter(f => /service/i.test(f)).length,
668
+ mappers: 0,
669
+ dtos: libFiles.filter(f => /dto|vo|model/i.test(f)).length,
670
+ totalFiles: libFiles.length,
671
+ modules: [libName],
672
+ serverTypes: ["library"],
673
+ pattern: "kotlin-library",
674
+ });
675
+ }
676
+ }
677
+
678
+ // Resolve shared query modules: redistribute internal domains to actual domain entries
679
+ resolveSharedQueryDomains(backendDomains, ktFiles);
680
+
681
+ // ── Kotlin single-module fallback (no multi-module structure detected) ──
682
+ if (backendDomains.length === 0 && ktFiles.length > 0) {
683
+ const ktDomains = {};
684
+ const skipNames = ["common", "config", "util", "utils", "base", "shared", "global", "framework", "infra", "main", "generated", "build"];
685
+ const layerKw = ["controller", "service", "repository", "mapper", "dao", "dto", "vo", "entity", "aggregate", "adapter"];
686
+ for (const f of ktFiles) {
687
+ const parts = f.replace(/\\/g, "/").split("/");
688
+ for (let i = 0; i < parts.length - 1; i++) {
689
+ if (layerKw.includes(parts[i].toLowerCase())) {
690
+ // domain/layer/ pattern
691
+ if (i > 0) {
692
+ const d = parts[i - 1].toLowerCase();
693
+ if (!skipNames.includes(d) && !layerKw.includes(d) && d.length > 1 && d !== "kotlin") {
694
+ if (!ktDomains[d]) ktDomains[d] = { controllers: 0, services: 0, mappers: 0, dtos: 0, totalFiles: 0 };
695
+ if (parts[i] === "controller") ktDomains[d].controllers++;
696
+ else if (parts[i] === "service") ktDomains[d].services++;
697
+ else if (["repository", "mapper", "dao"].includes(parts[i])) ktDomains[d].mappers++;
698
+ else if (["dto", "vo", "entity"].includes(parts[i])) ktDomains[d].dtos++;
699
+ ktDomains[d].totalFiles++;
700
+ }
701
+ }
702
+ break;
703
+ }
704
+ }
705
+ }
706
+ for (const [d, data] of Object.entries(ktDomains)) {
707
+ if (data.totalFiles > 0) {
708
+ backendDomains.push({ name: d, type: "backend", ...data, pattern: "kotlin-single" });
709
+ }
710
+ }
711
+ }
712
+ }
713
+
714
+ // ── Node.js backend (Express/NestJS) — scan regardless of frontend presence ──
401
715
  if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework) {
402
716
  const nestModules = await glob("src/modules/*/", { cwd: ROOT });
403
717
  const srcDirs = nestModules.length > 0 ? nestModules : await glob("src/*/", { cwd: ROOT });
@@ -417,7 +731,7 @@ async function scanStructure(stack) {
417
731
 
418
732
  // ── Next.js/React/Vue ──
419
733
  if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
420
- // App Router / Pages Router 도메인
734
+ // App Router / Pages Router domains
421
735
  const allDirs = [
422
736
  ...await glob("{app,src/app}/*/", { cwd: ROOT }),
423
737
  ...await glob("{pages,src/pages}/*/", { cwd: ROOT }),
@@ -456,7 +770,7 @@ async function scanStructure(stack) {
456
770
  }
457
771
  }
458
772
 
459
- // components/* (기존)
773
+ // components/* (existing)
460
774
  const compDirs = await glob("{src/,}components/*/", { cwd: ROOT });
461
775
  for (const dir of compDirs) {
462
776
  const name = path.basename(dir);
@@ -467,7 +781,7 @@ async function scanStructure(stack) {
467
781
  }
468
782
  }
469
783
 
470
- // ── 폴백: 스캐너로 0개일 page.tsx/index.tsx 위치에서 직접 도메인 추출 ──
784
+ // ── Fallback: extract domains directly from page.tsx/index.tsx locations when scanners return 0 ──
471
785
  if (frontendDomains.length === 0) {
472
786
  const pageFiles = await glob("**/page.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
473
787
  const domainSet = {};
@@ -486,7 +800,7 @@ async function scanStructure(stack) {
486
800
  }
487
801
  }
488
802
  }
489
- // client.tsx 카운트
803
+ // Count client.tsx as well
490
804
  const clientFiles = await glob("**/client.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
491
805
  for (const f of clientFiles) {
492
806
  const parts = f.replace(/\\/g, "/").split("/");
@@ -507,7 +821,7 @@ async function scanStructure(stack) {
507
821
  });
508
822
  }
509
823
 
510
- // widgets/features/entities 직접 스캔 (글로브 폴백)
824
+ // Also scan widgets/features/entities directly (glob fallback)
511
825
  for (const layer of ["widgets", "features", "entities"]) {
512
826
  const layerFiles = await glob(`**/${layer}/*/**/*.{tsx,jsx,ts,js}`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/*.spec.*", "**/*.test.*"] });
513
827
  const layerDomains = {};
@@ -537,9 +851,9 @@ async function scanStructure(stack) {
537
851
  if (dir === "." || dir.includes("venv")) continue;
538
852
  const name = path.basename(dir);
539
853
  const appFiles = await glob(`${dir}/*.py`, { cwd: ROOT });
540
- const views = appFiles.filter(f => f.includes("views")).length;
541
- const models = appFiles.filter(f => f.includes("models")).length;
542
- const serializers = appFiles.filter(f => f.includes("serializers")).length;
854
+ const views = appFiles.filter(x => x.includes("views")).length;
855
+ const models = appFiles.filter(x => x.includes("models")).length;
856
+ const serializers = appFiles.filter(x => x.includes("serializers")).length;
543
857
  backendDomains.push({ name, type: "backend", views, models, serializers, totalFiles: appFiles.length });
544
858
  }
545
859
  }
@@ -567,7 +881,7 @@ async function scanStructure(stack) {
567
881
  }
568
882
  }
569
883
 
570
- // 프론트엔드 카운트
884
+ // Frontend count
571
885
  const frontend = { exists: false, components: 0, pages: 0, hooks: 0 };
572
886
  if (stack.frontend) {
573
887
  frontend.exists = true;
@@ -576,7 +890,7 @@ async function scanStructure(stack) {
576
890
  frontend.hooks = (await glob("{src/,}**/hooks/**/*.{ts,js}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
577
891
  }
578
892
 
579
- // App Router RSC/Client 전체 통계 (project-analysis.json)
893
+ // App Router RSC/Client overall stats (for project-analysis.json)
580
894
  if (stack.frontend === "nextjs") {
581
895
  const allClientFiles = await glob("{app,src/app}/**/client.{tsx,ts,jsx,js}", { cwd: ROOT });
582
896
  const allPageFiles = await glob("{app,src/app}/**/page.{tsx,ts,jsx,js}", { cwd: ROOT });
@@ -587,7 +901,7 @@ async function scanStructure(stack) {
587
901
  frontend.rscPattern = allClientFiles.length > 0;
588
902
  }
589
903
 
590
- // 전체 도메인 = 백엔드 + 프론트엔드 (type 태그 포함)
904
+ // All domains = backend + frontend (with type tags)
591
905
  const allDomains = [
592
906
  ...backendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
593
907
  ...frontendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
@@ -596,7 +910,201 @@ async function scanStructure(stack) {
596
910
  return { domains: allDomains, backendDomains, frontendDomains, rootPackage, frontend };
597
911
  }
598
912
 
599
- // ─── 도메인 그룹 분할 (타입별) ──────────────────────────
913
+ // ─── Resolve shared query modules ────────────────────────────────
914
+ // When a shared query module (e.g., common-query-server) contains controllers/services
915
+ // for multiple domains, extract the actual domains and merge them into existing entries.
916
+ // Supports two extraction patterns:
917
+ // A) Package-based: .../kotlin/com/company/.../DOMAIN/controller/XxxController.kt
918
+ // B) Class-name-based: .../controller/ReservationQueryController.kt → "reservation"
919
+ // Safety: if no shared modules are found, this function does nothing (no-op).
920
+ function resolveSharedQueryDomains(backendDomains, ktFiles) {
921
+ const genericNames = ["common", "shared", "base", "core", "global"];
922
+ const layerNames = new Set([
923
+ "controller", "service", "repository", "mapper", "api", "handler",
924
+ "dto", "model", "entity", "config", "configuration", "util", "utils",
925
+ "infra", "infrastructure", "framework", "common", "shared", "base",
926
+ "core", "global", "internal", "external", "exception", "interceptor",
927
+ "filter", "converter", "event", "listener", "client", "feign",
928
+ "adapter", "port", "domain", "application", "presentation",
929
+ "persistence", "web", "rest", "grpc", "query", "command",
930
+ "aggregate", "aggregator", "vo", "valueobject",
931
+ ]);
932
+ const skipPackages = new Set([
933
+ "com", "org", "net", "io", "kr", "jp", "cn", "de", "fr", "uk", "us",
934
+ "dev", "app", "main", "server", "backend", "project", "kotlin",
935
+ ]);
936
+ const classSuffixes = new Set([
937
+ "Controller", "Service", "Repository", "Mapper", "Handler", "Api",
938
+ "Query", "Command", "Read", "Write", "Get", "Find", "List", "Search",
939
+ "Admin", "Dto", "Request", "Response", "Entity", "Model", "Impl",
940
+ "Factory", "Builder", "Adapter", "Client", "Facade", "Provider",
941
+ ]);
942
+
943
+ // Find shared modules: generic name + query server type
944
+ const sharedModules = backendDomains.filter(d =>
945
+ d.pattern === "kotlin-multimodule" &&
946
+ d.serverTypes && d.serverTypes.includes("query") &&
947
+ genericNames.some(g => d.name.startsWith(g))
948
+ );
949
+ if (sharedModules.length === 0) return;
950
+
951
+ // Existing domain names for Pattern B-1 matching (longest-first for greedy match)
952
+ const existingDomains = backendDomains
953
+ .filter(d => !sharedModules.includes(d) && d.pattern === "kotlin-multimodule")
954
+ .map(d => d.name)
955
+ .sort((a, b) => b.length - a.length);
956
+
957
+ for (const shared of sharedModules) {
958
+ const moduleNames = shared.modules || [];
959
+ const sharedKtFiles = ktFiles.filter(f =>
960
+ moduleNames.some(m => f.includes(`/${m}/`) || f.startsWith(`${m}/`))
961
+ );
962
+ if (sharedKtFiles.length === 0) continue;
963
+
964
+ const domainFileMap = {}; // { domainName: [filePaths] }
965
+
966
+ for (const filePath of sharedKtFiles) {
967
+ let domain = null;
968
+ const parts = filePath.replace(/\\/g, "/").split("/");
969
+
970
+ // ── Pattern A: Package-based ──
971
+ // Looks for: .../kotlin/com/company/[project/]DOMAIN/LAYER/File.kt
972
+ // Uses depth >= 3 from kotlin/ to skip base package segments
973
+ const kotlinIdx = parts.findIndex(p => p === "kotlin");
974
+ if (kotlinIdx >= 0) {
975
+ for (let i = kotlinIdx + 1; i < parts.length - 1; i++) {
976
+ if (layerNames.has(parts[i].toLowerCase())) {
977
+ if (i - 1 > kotlinIdx) {
978
+ const candidate = parts[i - 1].toLowerCase();
979
+ const depth = (i - 1) - kotlinIdx; // distance from kotlin/
980
+ if (
981
+ depth >= 3 &&
982
+ !layerNames.has(candidate) &&
983
+ !skipPackages.has(candidate) &&
984
+ candidate.length > 2
985
+ ) {
986
+ domain = candidate;
987
+ }
988
+ }
989
+ break; // only check first layer directory encountered
990
+ }
991
+ }
992
+ }
993
+
994
+ // ── Pattern B-1: Match class name against existing domain names ──
995
+ if (!domain) {
996
+ const fileName = parts[parts.length - 1].replace(/\.kt$/, "").toLowerCase();
997
+ for (const ed of existingDomains) {
998
+ const normalized = ed.replace(/-/g, "");
999
+ if (fileName.startsWith(normalized) && fileName.length > normalized.length) {
1000
+ domain = ed;
1001
+ break;
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ // ── Pattern B-2: Extract from PascalCase class name ──
1007
+ if (!domain) {
1008
+ const fileName = parts[parts.length - 1].replace(/\.kt$/, "");
1009
+ const words = fileName.match(/[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|$)/g);
1010
+ if (words && words.length >= 2) {
1011
+ const domainWords = [];
1012
+ for (const w of words) {
1013
+ if (classSuffixes.has(w)) break;
1014
+ domainWords.push(w);
1015
+ }
1016
+ if (domainWords.length > 0) {
1017
+ const extracted = domainWords.join("").toLowerCase();
1018
+ if (
1019
+ !genericNames.includes(extracted) &&
1020
+ !skipPackages.has(extracted) &&
1021
+ extracted.length > 2
1022
+ ) {
1023
+ domain = extracted;
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ if (domain) {
1030
+ if (!domainFileMap[domain]) domainFileMap[domain] = [];
1031
+ domainFileMap[domain].push(filePath);
1032
+ }
1033
+ }
1034
+
1035
+ // ── Merge extracted domains into backendDomains ──
1036
+ let distributedFiles = 0;
1037
+ for (const [domain, files] of Object.entries(domainFileMap)) {
1038
+ // Find matching existing domain (exact or normalized)
1039
+ const existing = backendDomains.find(d =>
1040
+ d !== shared &&
1041
+ (d.name === domain || d.name.replace(/-/g, "") === domain.replace(/-/g, ""))
1042
+ );
1043
+
1044
+ const ctrlCount = files.filter(f =>
1045
+ /\/controller\//i.test(f) || /Controller\.kt$/i.test(f)
1046
+ ).length;
1047
+ const svcCount = files.filter(f =>
1048
+ /\/service\//i.test(f) || /Service\.kt$/i.test(f)
1049
+ ).length;
1050
+ const repoCount = files.filter(f =>
1051
+ /\/repository\//i.test(f) || /\/mapper\//i.test(f) ||
1052
+ /Repository\.kt$/i.test(f) || /Mapper\.kt$/i.test(f)
1053
+ ).length;
1054
+ const dtoCount = files.filter(f =>
1055
+ (/\/dto\//i.test(f) || /\/vo\//i.test(f) || /Dto\.kt$/i.test(f) ||
1056
+ /Vo\.kt$/i.test(f) || /Request\.kt$/i.test(f) || /Response\.kt$/i.test(f)) &&
1057
+ !/\/controller\//i.test(f) && !/\/service\//i.test(f) &&
1058
+ !/\/repository\//i.test(f)
1059
+ ).length;
1060
+
1061
+ if (existing) {
1062
+ // Merge into existing domain
1063
+ if (!existing.modules) existing.modules = [];
1064
+ for (const m of moduleNames) {
1065
+ if (!existing.modules.includes(m)) existing.modules.push(m);
1066
+ }
1067
+ if (!existing.serverTypes) existing.serverTypes = [];
1068
+ if (!existing.serverTypes.includes("query")) existing.serverTypes.push("query");
1069
+ existing.controllers += ctrlCount;
1070
+ existing.services += svcCount;
1071
+ if (existing.mappers != null) existing.mappers += repoCount;
1072
+ existing.dtos += dtoCount;
1073
+ existing.totalFiles += files.length;
1074
+ } else {
1075
+ // Create new domain entry
1076
+ backendDomains.push({
1077
+ name: domain,
1078
+ type: "backend",
1079
+ controllers: ctrlCount,
1080
+ services: svcCount,
1081
+ mappers: repoCount,
1082
+ dtos: dtoCount,
1083
+ totalFiles: files.length,
1084
+ modules: [...moduleNames],
1085
+ serverTypes: ["query"],
1086
+ pattern: "kotlin-multimodule",
1087
+ resolvedFrom: shared.name,
1088
+ });
1089
+ }
1090
+ distributedFiles += files.length;
1091
+ }
1092
+
1093
+ // Adjust shared module: keep only undistributed files
1094
+ if (distributedFiles > 0) {
1095
+ shared.totalFiles = Math.max(0, shared.totalFiles - distributedFiles);
1096
+ shared.resolvedDomains = Object.keys(domainFileMap);
1097
+ if (shared.totalFiles === 0) shared._resolved = true;
1098
+ }
1099
+ }
1100
+
1101
+ // Remove fully resolved shared entries (all files distributed)
1102
+ for (let i = backendDomains.length - 1; i >= 0; i--) {
1103
+ if (backendDomains[i]._resolved) backendDomains.splice(i, 1);
1104
+ }
1105
+ }
1106
+
1107
+ // ─── Domain group splitting (per type) ──────────────────────────
600
1108
  function splitDomainGroups(domains, type, template) {
601
1109
  const MAX_FILES_PER_GROUP = 40;
602
1110
  const MAX_DOMAINS_PER_GROUP = 4;
@@ -620,7 +1128,7 @@ function splitDomainGroups(domains, type, template) {
620
1128
  return groups;
621
1129
  }
622
1130
 
623
- // ─── 활성 도메인 결정 ───────────────────────────────────
1131
+ // ─── Determine active domains ───────────────────────────────────
624
1132
  function determineActiveDomains(stack) {
625
1133
  const isBackend = stack.framework && !["nextjs", "react", "vue"].includes(stack.framework);
626
1134
  return {
@@ -634,24 +1142,25 @@ function determineActiveDomains(stack) {
634
1142
  };
635
1143
  }
636
1144
 
637
- // ─── 템플릿 선택 (멀티스택) ──────────────────────────────
1145
+ // ─── Template selection (multi-stack) ──────────────────────────────
638
1146
  function selectTemplates(stack) {
639
1147
  const templates = { backend: null, frontend: null };
640
1148
 
641
- // 백엔드 템플릿
642
- if (stack.language === "java") templates.backend = "java-spring";
1149
+ // Backend template
1150
+ if (stack.language === "kotlin") templates.backend = "kotlin-spring";
1151
+ else if (stack.language === "java") templates.backend = "java-spring";
643
1152
  else if (stack.framework === "express" || stack.framework === "nestjs") templates.backend = "node-express";
644
1153
  else if (stack.framework === "django") templates.backend = "python-django";
645
1154
  else if (stack.framework === "fastapi" || stack.framework === "flask") templates.backend = "python-fastapi";
646
1155
  else if (stack.language === "typescript" || stack.language === "javascript") templates.backend = "node-express";
647
1156
  else if (stack.language === "python") templates.backend = "python-fastapi";
648
1157
 
649
- // 프론트엔드 템플릿
1158
+ // Frontend template
650
1159
  if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
651
1160
  templates.frontend = "node-nextjs";
652
1161
  }
653
1162
 
654
- // 프론트엔드만 있고 백엔드가 없는 순수 프론트엔드 프로젝트
1163
+ // Pure frontend project with no backend
655
1164
  if (!templates.backend && templates.frontend) {
656
1165
  templates.backend = null;
657
1166
  }
@@ -659,28 +1168,43 @@ function selectTemplates(stack) {
659
1168
  return templates;
660
1169
  }
661
1170
 
662
- // ─── 프롬프트 동적 생성 (멀티스택) ──────────────────────
663
- function generatePrompts(templates) {
1171
+ // ─── Dynamic prompt generation (multi-stack) ──────────────────────
1172
+ function generatePrompts(templates, lang) {
664
1173
  const commonDir = path.join(TEMPLATES_DIR, "common");
665
1174
  const headerPath = path.join(commonDir, "header.md");
666
1175
  const footerPath = path.join(commonDir, "pass3-footer.md");
1176
+ const langPath = path.join(commonDir, "lang-instructions.json");
667
1177
 
668
1178
  const header = fs.existsSync(headerPath) ? fs.readFileSync(headerPath, "utf-8") : "";
669
1179
  const footer = fs.existsSync(footerPath) ? fs.readFileSync(footerPath, "utf-8") : "";
670
1180
 
1181
+ // Load language instruction for Pass 3 (analysis passes stay English)
1182
+ let langInstruction = "";
1183
+ if (lang && lang !== "en" && fs.existsSync(langPath)) {
1184
+ try {
1185
+ const langData = JSON.parse(fs.readFileSync(langPath, "utf-8"));
1186
+ langInstruction = langData.instructions[lang] || "";
1187
+ if (langInstruction) {
1188
+ console.log(` 🌐 Language: ${langData.labels[lang]} (Pass 3 output)`);
1189
+ }
1190
+ } catch (e) {
1191
+ console.warn(` ⚠️ lang-instructions.json parse error: ${e.message}`);
1192
+ }
1193
+ }
1194
+
671
1195
  function readTemplate(templateName, passName) {
672
1196
  const src = path.join(TEMPLATES_DIR, templateName, `${passName}.md`);
673
1197
  if (!fs.existsSync(src)) return null;
674
1198
  let body = fs.readFileSync(src, "utf-8");
675
- body = body.replace(/^프로젝트 루트 경로:.*\n이 경로를 기준으로.*\n\n?---\n\n?/s, "");
676
- body = body.replace(/\n완료 반드시 아래 명령을 순서대로 실행:[\s\S]*$/m, "");
1199
+ body = body.replace(/^Project root path:.*\nInterpret all file paths.*\n\n?---\n\n?/s, "");
1200
+ body = body.replace(/\nAfter completion, run the following commands in order:[\s\S]*$/m, "");
677
1201
  return body;
678
1202
  }
679
1203
 
680
1204
  const activeTemplates = [templates.backend, templates.frontend].filter(Boolean);
681
1205
  const primaryTemplate = templates.backend || templates.frontend;
682
1206
 
683
- // ─── Pass 1: 타입별 별도 프롬프트 ──────────────────────
1207
+ // ─── Pass 1: Separate prompts per type ──────────────────────
684
1208
  for (const tmpl of activeTemplates) {
685
1209
  const type = tmpl === templates.frontend ? "frontend" : "backend";
686
1210
  const body = readTemplate(tmpl, "pass1");
@@ -691,7 +1215,7 @@ function generatePrompts(templates) {
691
1215
  }
692
1216
  }
693
1217
 
694
- // 단일 스택 하위 호환: pass1-prompt.md 생성 (primary 기준)
1218
+ // Single-stack backward compat: also generate pass1-prompt.md (primary-based)
695
1219
  if (primaryTemplate) {
696
1220
  const body = readTemplate(primaryTemplate, "pass1");
697
1221
  if (body) {
@@ -699,7 +1223,7 @@ function generatePrompts(templates) {
699
1223
  }
700
1224
  }
701
1225
 
702
- // ─── Pass 2: 단일 프롬프트 (primary 기준, 모든 pass1 결과를 머지) ──
1226
+ // ─── Pass 2: Single prompt (primary-based, merges all pass1 results) ──
703
1227
  if (primaryTemplate) {
704
1228
  const body = readTemplate(primaryTemplate, "pass2");
705
1229
  if (body) {
@@ -708,20 +1232,20 @@ function generatePrompts(templates) {
708
1232
  }
709
1233
  }
710
1234
 
711
- // ─── Pass 3: 통합 프롬프트 (백엔드 + 프론트엔드 결합) ──
1235
+ // ─── Pass 3: Combined prompt (backend + frontend merged) ──
712
1236
  if (primaryTemplate) {
713
1237
  const primaryBody = readTemplate(primaryTemplate, "pass3");
714
1238
  let combinedBody = primaryBody || "";
715
1239
 
716
- // 멀티스택: 프론트엔드 pass3의 고유 섹션을 백엔드 pass3 병합
1240
+ // Multi-stack: merge frontend-specific sections from frontend pass3 into backend pass3
717
1241
  if (templates.backend && templates.frontend && templates.backend !== templates.frontend) {
718
1242
  const frontendBody = readTemplate(templates.frontend, "pass3");
719
1243
  if (frontendBody) {
720
1244
  combinedBody += "\n\n---\n\n";
721
- combinedBody += "# 추가: 프론트엔드 생성 대상 (자동 감지됨)\n\n";
722
- combinedBody += " 백엔드 표준에 더해, 아래 프론트엔드 표준도 함께 생성해줘.\n";
723
- combinedBody += "pass2-merged.json의 프론트엔드 분석 결과를 참조할 것.\n\n";
724
- // 프론트엔드 pass3에서 standard 섹션만 추출 (CLAUDE.md, guide 등 중복 제외)
1245
+ combinedBody += "# Additional: Frontend generation targets (auto-detected)\n\n";
1246
+ combinedBody += "In addition to the backend standards above, also generate the following frontend standards.\n";
1247
+ combinedBody += "Reference the frontend analysis results in pass2-merged.json.\n\n";
1248
+ // Extract only standard sections from frontend pass3 (exclude CLAUDE.md, guide duplicates)
725
1249
  const frontendSections = frontendBody
726
1250
  .split(/\n(?=\d+\.\s)/)
727
1251
  .filter(s => /frontend|component|page|routing|data.fetch|state|styling/i.test(s))
@@ -733,12 +1257,12 @@ function generatePrompts(templates) {
733
1257
  }
734
1258
 
735
1259
  const dst = path.join(GENERATED_DIR, "pass3-prompt.md");
736
- fs.writeFileSync(dst, header + combinedBody.trimEnd() + "\n" + footer);
1260
+ fs.writeFileSync(dst, header + langInstruction + combinedBody.trimEnd() + "\n" + footer);
737
1261
  console.log(` ✅ pass3-prompt.md${templates.frontend && templates.backend ? " (multi-stack combined)" : ""}`);
738
1262
  }
739
1263
  }
740
1264
 
741
- // ─── 메인 ───────────────────────────────────────────────
1265
+ // ─── Main ───────────────────────────────────────────────
742
1266
  async function main() {
743
1267
  console.log("\n╔══════════════════════════════════════╗");
744
1268
  console.log("║ ClaudeOS-Core — Plan Installer ║");
@@ -746,7 +1270,7 @@ async function main() {
746
1270
 
747
1271
  ensureDir(GENERATED_DIR);
748
1272
 
749
- // Phase 1: 스택 감지
1273
+ // Phase 1: Stack detection
750
1274
  console.log(" [Phase 1] Detecting stack...");
751
1275
  const stack = await detectStack();
752
1276
  console.log(` Language: ${stack.language || "unknown"} ${stack.languageVersion || ""}`);
@@ -756,7 +1280,7 @@ async function main() {
756
1280
  console.log(` ORM: ${stack.orm || "none"}`);
757
1281
  console.log(` PackageMgr: ${stack.packageManager || "none"}\n`);
758
1282
 
759
- // Phase 2: 구조 스캔
1283
+ // Phase 2: Structure scan
760
1284
  console.log(" [Phase 2] Scanning structure...");
761
1285
  const { domains, backendDomains, frontendDomains, rootPackage, frontend } = await scanStructure(stack);
762
1286
  console.log(` Backend: ${backendDomains.length} domains`);
@@ -766,7 +1290,7 @@ async function main() {
766
1290
  if (frontend.exists) console.log(` Components: ${frontend.components} components, ${frontend.pages} pages, ${frontend.hooks} hooks`);
767
1291
  console.log();
768
1292
 
769
- // Phase 3: 템플릿 선택 (멀티스택)
1293
+ // Phase 3: Template selection (multi-stack)
770
1294
  console.log(" [Phase 3] Selecting templates...");
771
1295
  const templates = selectTemplates(stack);
772
1296
  const isMultiStack = !!(templates.backend && templates.frontend);
@@ -776,7 +1300,7 @@ async function main() {
776
1300
  else console.log(` Mode: Single-stack`);
777
1301
  console.log();
778
1302
 
779
- // Phase 4: 도메인 그룹 분할 (타입별)
1303
+ // Phase 4: Domain group splitting (per type)
780
1304
  console.log(" [Phase 4] Splitting domain groups...");
781
1305
  const allGroups = [];
782
1306
  if (templates.backend && backendDomains.length > 0) {
@@ -787,7 +1311,7 @@ async function main() {
787
1311
  const fg = splitDomainGroups(frontendDomains, "frontend", templates.frontend);
788
1312
  allGroups.push(...fg);
789
1313
  }
790
- // passNum 재할당
1314
+ // Reassign passNum
791
1315
  allGroups.forEach((g, i) => { g.passNum = i + 1; });
792
1316
  allGroups.forEach((g, i) => {
793
1317
  const icon = g.type === "backend" ? "⚙️" : "🎨";
@@ -795,7 +1319,7 @@ async function main() {
795
1319
  });
796
1320
  console.log();
797
1321
 
798
- // Phase 5: 활성 도메인
1322
+ // Phase 5: Active domains
799
1323
  console.log(" [Phase 5] Active domains...");
800
1324
  const active = determineActiveDomains(stack);
801
1325
  Object.entries(active).forEach(([k, v]) => {
@@ -803,15 +1327,17 @@ async function main() {
803
1327
  });
804
1328
  console.log();
805
1329
 
806
- // Phase 6: 프롬프트 생성 (멀티스택)
807
- console.log(` [Phase 6] Generating prompts...`);
808
- generatePrompts(templates);
1330
+ // Phase 6: Prompt generation (multi-stack)
1331
+ const lang = process.env.CLAUDEOS_LANG || "en";
1332
+ console.log(` [Phase 6] Generating prompts (lang: ${lang})...`);
1333
+ generatePrompts(templates, lang);
809
1334
  console.log();
810
1335
 
811
- // 저장: project-analysis.json
1336
+ // Save: project-analysis.json
812
1337
  const defaultPort = stack.framework === "fastapi" || stack.framework === "django" ? 8000 : 8080;
813
1338
  const analysis = {
814
1339
  analyzedAt: new Date().toISOString(),
1340
+ lang,
815
1341
  stack: { ...stack, port: stack.port || defaultPort },
816
1342
  templates,
817
1343
  isMultiStack,
@@ -831,7 +1357,7 @@ async function main() {
831
1357
  fs.writeFileSync(path.join(GENERATED_DIR, "project-analysis.json"), JSON.stringify(analysis, null, 2));
832
1358
  console.log(" 💾 project-analysis.json saved");
833
1359
 
834
- // 저장: domain-groups.json
1360
+ // Save: domain-groups.json
835
1361
  const domainGroups = {
836
1362
  generatedAt: new Date().toISOString(),
837
1363
  isMultiStack,