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
package/bootstrap.sh CHANGED
@@ -2,10 +2,17 @@
2
2
 
3
3
  # ClaudeOS-Core — Bootstrap (3-Pass Auto)
4
4
  #
5
- # 원클릭 전체 시스템 자동 구축
6
- # 프로젝트 크기에 따라 Pass 1 자동으로 N 분할 실행
5
+ # One-click full system auto-build
6
+ # Automatically splits Pass 1 into N runs based on project size
7
7
  #
8
- # 실행: bash claudeos-core-tools/bootstrap.sh
8
+ # Usage: bash claudeos-core-tools/bootstrap.sh --lang ko
9
+ # bash claudeos-core-tools/bootstrap.sh (interactive)
10
+
11
+ # Require bash (not sh/dash) — script uses bash arrays and [[ ]]
12
+ if [ -z "$BASH_VERSION" ]; then
13
+ echo "❌ This script requires bash. Run with: bash $0" >&2
14
+ exit 1
15
+ fi
9
16
 
10
17
  set -e
11
18
 
@@ -16,9 +23,63 @@ GENERATED_DIR="$PROJECT_ROOT/claudeos-core/generated"
16
23
 
17
24
  cd "$PROJECT_ROOT"
18
25
 
19
- # ─── 프롬프트 플레이스홀더 치환 헬퍼 ────────────────────
26
+ # ─── Language selection (required) ──────────────────────────────
27
+ SUPPORTED_LANGS=("en" "ko" "zh-CN" "ja" "es" "vi" "hi" "ru" "fr" "de")
28
+ LANG_LABELS=("English" "한국어 (Korean)" "简体中文 (Chinese Simplified)" "日本語 (Japanese)" "Español (Spanish)" "Tiếng Việt (Vietnamese)" "हिन्दी (Hindi)" "Русский (Russian)" "Français (French)" "Deutsch (German)")
29
+
30
+ CLAUDEOS_LANG=""
31
+
32
+ # Parse --lang argument
33
+ while [[ $# -gt 0 ]]; do
34
+ case $1 in
35
+ --lang) CLAUDEOS_LANG="$2"; shift 2 ;;
36
+ --lang=*) CLAUDEOS_LANG="${1#*=}"; shift ;;
37
+ *) echo "⚠️ Unknown argument: $1 (ignored)"; shift ;;
38
+ esac
39
+ done
40
+
41
+ # Interactive selection if --lang not provided
42
+ if [ -z "$CLAUDEOS_LANG" ]; then
43
+ echo ""
44
+ echo "╔══════════════════════════════════════════════════╗"
45
+ echo "║ Select output language (required) ║"
46
+ echo "╚══════════════════════════════════════════════════╝"
47
+ echo ""
48
+ for i in "${!SUPPORTED_LANGS[@]}"; do
49
+ printf " %2d. %-6s — %s\n" "$((i+1))" "${SUPPORTED_LANGS[$i]}" "${LANG_LABELS[$i]}"
50
+ done
51
+ echo ""
52
+ read -p " Enter number (1-10) or language code: " LANG_INPUT
53
+
54
+ # Accept number
55
+ if [[ "$LANG_INPUT" =~ ^[0-9]+$ ]] && [ "$LANG_INPUT" -ge 1 ] && [ "$LANG_INPUT" -le 10 ]; then
56
+ CLAUDEOS_LANG="${SUPPORTED_LANGS[$((LANG_INPUT-1))]}"
57
+ else
58
+ # Accept language code
59
+ CLAUDEOS_LANG="$LANG_INPUT"
60
+ fi
61
+ fi
62
+
63
+ # Validate
64
+ VALID=false
65
+ for l in "${SUPPORTED_LANGS[@]}"; do
66
+ if [ "$l" = "$CLAUDEOS_LANG" ]; then VALID=true; break; fi
67
+ done
68
+ if [ "$VALID" = false ]; then
69
+ echo ""
70
+ printf ' ❌ Unsupported language: "%s"\n' "$CLAUDEOS_LANG"
71
+ echo " Supported: ${SUPPORTED_LANGS[*]}"
72
+ echo ""
73
+ exit 1
74
+ fi
75
+
76
+ export CLAUDEOS_LANG
77
+
78
+ # ─── Prompt placeholder substitution helper ────────────────────
20
79
  inject_project_root() {
21
- sed "s|{{PROJECT_ROOT}}|${PROJECT_ROOT}|g"
80
+ # Use perl with $ENV{} to avoid sed metachar issues with &, \, etc. in paths
81
+ export _PROJECT_ROOT="$PROJECT_ROOT"
82
+ perl -pe 's/\{\{PROJECT_ROOT\}\}/$ENV{_PROJECT_ROOT}/g'
22
83
  }
23
84
 
24
85
  echo ""
@@ -26,21 +87,28 @@ echo "╔═══════════════════════
26
87
  echo "║ ClaudeOS-Core — Bootstrap (3-Pass) ║"
27
88
  echo "╚════════════════════════════════════════════════════╝"
28
89
  echo " Project root: $PROJECT_ROOT"
90
+ # Find label for selected lang
91
+ for i in "${!SUPPORTED_LANGS[@]}"; do
92
+ if [ "${SUPPORTED_LANGS[$i]}" = "$CLAUDEOS_LANG" ]; then
93
+ echo " Language: ${LANG_LABELS[$i]} ($CLAUDEOS_LANG)"
94
+ break
95
+ fi
96
+ done
29
97
  echo ""
30
98
 
31
- # ─── [1] 의존성 설치 ────────────────────────────────────
99
+ # ─── [1] Install dependencies ────────────────────────────────────
32
100
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
33
101
  echo "[1] Installing dependencies..."
34
102
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
35
103
  cd "$TOOLS_DIR"
36
104
  if [ ! -d "node_modules" ]; then
37
- npm install --silent 2>/dev/null
105
+ npm install --silent
38
106
  fi
39
107
  cd "$PROJECT_ROOT"
40
108
  echo " ✅ Done"
41
109
  echo ""
42
110
 
43
- # ─── [2] 디렉토리 구조 생성 ─────────────────────────────
111
+ # ─── [2] Create directory structure ─────────────────────────────
44
112
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
45
113
  echo "[2] Creating directory structure..."
46
114
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -55,25 +123,31 @@ mkdir -p claudeos-core/mcp-guide
55
123
  echo " ✅ Done"
56
124
  echo ""
57
125
 
58
- # ─── [3] plan-installer 실행 (프로젝트 분석) ────────────
126
+ # ─── [3] Run plan-installer (project analysis) ────────────
59
127
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
60
128
  echo "[3] Analyzing project (plan-installer)..."
61
129
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
62
130
  node "$TOOLS_DIR/plan-installer/index.js"
63
131
  echo ""
64
132
 
65
- # ─── [4] Pass 1: 도메인 그룹별 분석 (멀티스택) ──────────
133
+ # ─── [4] Pass 1: Deep analysis per domain group (multi-stack) ──────────
66
134
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
67
135
  echo "[4] Pass 1 — Deep analysis per domain group..."
68
136
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
69
137
 
70
- # domain-groups.json에서 그룹 읽기
138
+ # Read groups from domain-groups.json
71
139
  TOTAL_GROUPS=$(node -e "
72
140
  const g = require(process.argv[1]);
73
141
  console.log(g.totalGroups);
74
142
  " "$GENERATED_DIR/domain-groups.json")
75
143
 
76
- for i in $(seq 1 $TOTAL_GROUPS); do
144
+ # Validate TOTAL_GROUPS is a positive integer
145
+ if ! [[ "$TOTAL_GROUPS" =~ ^[0-9]+$ ]] || [ "$TOTAL_GROUPS" -lt 1 ]; then
146
+ echo " ❌ Invalid TOTAL_GROUPS: '${TOTAL_GROUPS}'. domain-groups.json may be malformed."
147
+ exit 1
148
+ fi
149
+
150
+ for i in $(seq 1 "$TOTAL_GROUPS"); do
77
151
  DOMAIN_LIST=$(node -e "
78
152
  const g = require(process.argv[1]);
79
153
  console.log(g.groups[process.argv[2] - 1].domains.join(', '));
@@ -90,28 +164,37 @@ for i in $(seq 1 $TOTAL_GROUPS); do
90
164
  echo ""
91
165
  echo " [Pass 1-${i}/${TOTAL_GROUPS}] (${GROUP_TYPE}) Analyzing: ${DOMAIN_LIST} (~${EST_FILES} files)"
92
166
 
93
- # 이미 완료된 Pass는 skip
167
+ # Skip already completed passes
94
168
  if [ -f "$GENERATED_DIR/pass1-${i}.json" ]; then
95
169
  echo " ⏭️ pass1-${i}.json already exists, skipping"
96
170
  continue
97
171
  fi
98
172
 
99
- # 타입별 프롬프트 선택
173
+ # Select prompt by type
100
174
  PROMPT_FILE="$GENERATED_DIR/pass1-${GROUP_TYPE}-prompt.md"
101
175
  if [ ! -f "$PROMPT_FILE" ]; then
102
176
  PROMPT_FILE="$GENERATED_DIR/pass1-prompt.md"
103
177
  fi
104
178
  PASS1_TEMPLATE=$(cat "$PROMPT_FILE")
105
179
 
106
- # 템플릿에서 플레이스홀더 치환
107
- PASS1_PROMPT=$(echo "$PASS1_TEMPLATE" | sed "s|{{DOMAIN_GROUP}}|${DOMAIN_LIST}|g" | sed "s|{{PASS_NUM}}|${i}|g" | inject_project_root)
180
+ # Substitute placeholders via temp file (avoids sed special char issues with &, \, etc.)
181
+ TMP_PROMPT="$GENERATED_DIR/_tmp_pass1_prompt.md"
182
+ printf '%s' "$PASS1_TEMPLATE" > "$TMP_PROMPT"
183
+ # Use perl with $ENV{} for safe literal replacement (no shell interpolation into Perl code)
184
+ export _DOMAIN_LIST="$DOMAIN_LIST"
185
+ export _PASS_NUM="$i"
186
+ perl -pi -e 's/\{\{DOMAIN_GROUP\}\}/$ENV{_DOMAIN_LIST}/g' "$TMP_PROMPT"
187
+ perl -pi -e 's/\{\{PASS_NUM\}\}/$ENV{_PASS_NUM}/g' "$TMP_PROMPT"
188
+ # inject_project_root uses perl with $ENV{} which is safe for special chars in paths
189
+ PASS1_PROMPT=$(cat "$TMP_PROMPT" | inject_project_root)
190
+ rm -f "$TMP_PROMPT"
108
191
 
109
192
  if ! (cd "$PROJECT_ROOT" && claude -p "$PASS1_PROMPT" --dangerously-skip-permissions); then
110
193
  echo " ❌ Pass 1-${i} failed. Aborting."
111
194
  exit 1
112
195
  fi
113
196
 
114
- # JSON 생성 여부 확인
197
+ # Verify JSON was created
115
198
  if [ ! -f "$GENERATED_DIR/pass1-${i}.json" ]; then
116
199
  echo " ❌ pass1-${i}.json was not created. Aborting."
117
200
  exit 1
@@ -121,7 +204,7 @@ for i in $(seq 1 $TOTAL_GROUPS); do
121
204
  done
122
205
  echo ""
123
206
 
124
- # ─── [5] Pass 2: 분석 결과 통합 ─────────────────────────
207
+ # ─── [5] Pass 2: Merge analysis results ─────────────────────────
125
208
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
126
209
  echo "[5] Pass 2 — Merging analysis results..."
127
210
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -145,7 +228,7 @@ else
145
228
  fi
146
229
  echo ""
147
230
 
148
- # ─── [6] Pass 3: 생성 + 마스터 플랜 빌드 + 검증 ────────
231
+ # ─── [6] Pass 3: Generate + Master Plan build + verification ────────
149
232
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
150
233
  echo "[6] Pass 3 — Generating all files..."
151
234
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -162,7 +245,31 @@ if [ ! -f "$PROJECT_ROOT/CLAUDE.md" ]; then
162
245
  fi
163
246
  echo ""
164
247
 
165
- # ─── 완료 ───────────────────────────────────────────────
248
+ # ─── [7] Run verification tools ───────────────────────────────
249
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
250
+ echo "[7] Running verification tools..."
251
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
252
+
253
+ if [ -f "$TOOLS_DIR/manifest-generator/index.js" ]; then
254
+ echo -n " ⏳ manifest-generator..."
255
+ if node "$TOOLS_DIR/manifest-generator/index.js" > /dev/null 2>&1; then
256
+ echo " ✅"
257
+ else
258
+ echo " ❌ (non-fatal)"
259
+ fi
260
+ fi
261
+
262
+ if [ -f "$TOOLS_DIR/health-checker/index.js" ]; then
263
+ echo -n " ⏳ health-checker..."
264
+ if node "$TOOLS_DIR/health-checker/index.js" > /dev/null 2>&1; then
265
+ echo " ✅"
266
+ else
267
+ echo " ⚠️ issues found (non-fatal)"
268
+ fi
269
+ fi
270
+ echo ""
271
+
272
+ # ─── Complete ───────────────────────────────────────────────
166
273
  TOTAL_FILES=$(find .claude claudeos-core -type f 2>/dev/null | grep -v node_modules | wc -l | tr -d ' ')
167
274
  TOTAL_GROUPS_DONE=$TOTAL_GROUPS
168
275
  PASS1_FILES=$(ls -1 "$GENERATED_DIR"/pass1-*.json 2>/dev/null | wc -l | tr -d ' ')
@@ -179,6 +286,6 @@ echo "║ Verify anytime: ║"
179
286
  echo "║ node claudeos-core-tools/health-checker/index.js ║"
180
287
  echo "║ ║"
181
288
  echo "║ Start using: ║"
182
- echo "║ \"CRUD 만들어줘\" ║"
289
+ echo "║ \"Create a CRUD for orders\" ║"
183
290
  echo "╚════════════════════════════════════════════════════╝"
184
291
  echo ""
@@ -3,17 +3,17 @@
3
3
  /**
4
4
  * ClaudeOS-Core — Content Validator
5
5
  *
6
- * 역할: 생성된 파일의 내용 품질을 검증
7
- * 검증 항목:
8
- * - 파일이 비어있지 않은지
9
- * - standard 파일에 ✅/❌ 예시 + 규칙 테이블이 있는지
10
- * - rules 파일에 paths: frontmatter + @import가 있는지
11
- * - CLAUDE.md 필수 섹션 존재 여부
12
- * - guide 9 파일 전부 생성됐는지
13
- * - skills 오케스트레이터 + 하위 스킬 존재 여부
14
- * - database/, mcp-guide/ 파일 생성 여부
6
+ * Role: Validate content quality of generated files
7
+ * Validation items:
8
+ * - File is not empty
9
+ * - standard files contain ✅/❌ examples + rules table
10
+ * - rules files contain paths: frontmatter + @import
11
+ * - CLAUDE.md required sections exist
12
+ * - All 9 guide files are generated
13
+ * - Skills orchestrator + sub-skills exist
14
+ * - database/, mcp-guide/ files are generated
15
15
  *
16
- * 실행: npx claudeos-core <cmd> 또는 node claudeos-core-tools/content-validator/index.js
16
+ * Usage: npx claudeos-core <cmd> or node claudeos-core-tools/content-validator/index.js
17
17
  */
18
18
 
19
19
  const fs = require("fs");
@@ -41,21 +41,51 @@ async function main() {
41
41
  const warnings = [];
42
42
  let checked = 0;
43
43
 
44
+ // ─── Detect language and stack from project-analysis.json ────────
45
+ let detectedLanguage = null;
46
+ let outputLang = "en";
47
+ const paPath = path.join(GEN_DIR, "project-analysis.json");
48
+ if (fs.existsSync(paPath)) {
49
+ try {
50
+ const pa = JSON.parse(fs.readFileSync(paPath, "utf-8"));
51
+ detectedLanguage = pa.stack?.language || null;
52
+ outputLang = pa.lang || "en";
53
+ } catch { /* ignore */ }
54
+ }
55
+
56
+ // Language-aware section keywords for CLAUDE.md validation
57
+ const SECTION_KEYWORDS = {
58
+ en: ["Role", "Build", "Run", "Standard", "Skills"],
59
+ ko: ["역할", "빌드", "실행", "표준", "스킬"],
60
+ "zh-CN": ["角色", "构建", "运行", "标准", "技能"],
61
+ ja: ["役割", "ビルド", "実行", "標準", "スキル"],
62
+ es: ["Rol", "Compilar", "Ejecutar", "Estándar", "Habilidades"],
63
+ vi: ["Vai trò", "Build", "Chạy", "Tiêu chuẩn", "Kỹ năng"],
64
+ hi: ["भूमिका", "बिल्ड", "रन", "मानक", "कौशल"],
65
+ ru: ["Роль", "Сборка", "Запуск", "Стандарт", "Навыки"],
66
+ fr: ["Rôle", "Build", "Exécuter", "Standard", "Compétences"],
67
+ de: ["Rolle", "Build", "Ausführen", "Standard", "Fähigkeiten"],
68
+ };
69
+
44
70
  // ─── 1. CLAUDE.md ──────────────────────────────────────
45
71
  console.log(" [1/8] CLAUDE.md...");
46
72
  const claudeMd = path.join(ROOT, "CLAUDE.md");
47
73
  if (!fs.existsSync(claudeMd)) {
48
- errors.push({ file: "CLAUDE.md", type: "MISSING", msg: "CLAUDE.md 존재하지 않음" });
74
+ errors.push({ file: "CLAUDE.md", type: "MISSING", msg: "CLAUDE.md does not exist" });
49
75
  } else {
50
76
  checked++;
51
77
  const content = fs.readFileSync(claudeMd, "utf-8");
52
78
  if (content.trim().length < 100) {
53
- errors.push({ file: "CLAUDE.md", type: "EMPTY", msg: "CLAUDE.md 내용이 너무 짧음 (<100)" });
79
+ errors.push({ file: "CLAUDE.md", type: "EMPTY", msg: "CLAUDE.md content is too short (<100 chars)" });
54
80
  }
55
- const requiredSections = ["Role", "Build", "Run", "Standard", "Skills"];
56
- for (const sec of requiredSections) {
57
- if (!content.toLowerCase().includes(sec.toLowerCase())) {
58
- warnings.push({ file: "CLAUDE.md", type: "MISSING_SECTION", msg: `'${sec}' 섹션이 없음` });
81
+ // Check sections in both English (fallback) and output language
82
+ const langKeywords = SECTION_KEYWORDS[outputLang] || SECTION_KEYWORDS.en;
83
+ const enKeywords = SECTION_KEYWORDS.en;
84
+ for (let i = 0; i < enKeywords.length; i++) {
85
+ const candidates = [enKeywords[i], langKeywords[i]].filter(Boolean);
86
+ const found = candidates.some(kw => content.toLowerCase().includes(kw.toLowerCase()));
87
+ if (!found) {
88
+ warnings.push({ file: "CLAUDE.md", type: "MISSING_SECTION", msg: `'${enKeywords[i]}' / '${langKeywords[i]}' section is missing` });
59
89
  }
60
90
  }
61
91
  }
@@ -69,22 +99,26 @@ async function main() {
69
99
  const c = fs.readFileSync(f, "utf-8");
70
100
  const r = rel(f);
71
101
  if (c.trim().length === 0) {
72
- errors.push({ file: r, type: "EMPTY", msg: " 파일" });
102
+ errors.push({ file: r, type: "EMPTY", msg: "Empty file" });
73
103
  continue;
74
104
  }
75
- // sync 규칙은 paths 불필요
105
+ // sync rules don't need paths
76
106
  if (!r.includes("50.sync")) {
77
- if (!c.includes("paths:") && !c.startsWith("---")) {
78
- warnings.push({ file: r, type: "NO_FRONTMATTER", msg: "paths: frontmatter 없음" });
107
+ const hasFrontmatter = c.startsWith("---");
108
+ const hasPathsKey = c.includes("paths:");
109
+ if (!hasFrontmatter) {
110
+ warnings.push({ file: r, type: "NO_FRONTMATTER", msg: "Missing YAML frontmatter (---)" });
111
+ } else if (!hasPathsKey) {
112
+ warnings.push({ file: r, type: "NO_PATHS", msg: "Frontmatter exists but missing paths: key" });
79
113
  }
80
114
  if (!/^@[^\s]+/m.test(c)) {
81
- warnings.push({ file: r, type: "NO_IMPORT", msg: "@import 참조 없음" });
115
+ warnings.push({ file: r, type: "NO_IMPORT", msg: "No @import reference found" });
82
116
  }
83
117
  }
84
118
  }
85
- console.log(` ${ruleFiles.length} 파일 검사`);
119
+ console.log(` ${ruleFiles.length} files checked`);
86
120
  } else {
87
- errors.push({ file: ".claude/rules/", type: "MISSING", msg: "rules 디렉토리 없음" });
121
+ errors.push({ file: ".claude/rules/", type: "MISSING", msg: "rules directory not found" });
88
122
  }
89
123
 
90
124
  // ─── 3. claudeos-core/standard/** ─────────────────────
@@ -96,25 +130,59 @@ async function main() {
96
130
  const c = fs.readFileSync(f, "utf-8");
97
131
  const r = rel(f);
98
132
  if (c.trim().length === 0) {
99
- errors.push({ file: r, type: "EMPTY", msg: " 파일" });
133
+ errors.push({ file: r, type: "EMPTY", msg: "Empty file" });
100
134
  continue;
101
135
  }
102
136
  if (c.length < 200) {
103
- warnings.push({ file: r, type: "TOO_SHORT", msg: `내용이 짧음 (${c.length})` });
137
+ warnings.push({ file: r, type: "TOO_SHORT", msg: `Content is short (${c.length} chars)` });
104
138
  }
105
- if (!c.includes("✅") && !c.includes("올바른") && !c.includes("correct") && !c.includes("GOOD")) {
106
- warnings.push({ file: r, type: "NO_GOOD_EXAMPLE", msg: "올바른 예시(✅)가 없음" });
139
+ // Language-aware ✅/❌ example detection (all 10 supported languages)
140
+ const goodKeywords = [
141
+ "✅", "Correct", "correct", "GOOD",
142
+ "올바른", // ko
143
+ "正确", // zh-CN
144
+ "正しい", // ja
145
+ "Correcto", // es
146
+ "Đúng", // vi
147
+ "सही", // hi
148
+ "Правильн", // ru
149
+ "Correct", // fr (same as en)
150
+ "Richtig", // de
151
+ ];
152
+ const badKeywords = [
153
+ "❌", "Incorrect", "incorrect", "BAD",
154
+ "잘못된", // ko
155
+ "错误", // zh-CN
156
+ "誤った", // ja
157
+ "Incorrecto", // es
158
+ "Sai", // vi
159
+ "गलत", // hi
160
+ "Неправильн", // ru
161
+ "Incorrect", // fr (same as en)
162
+ "Falsch", // de
163
+ ];
164
+ if (!goodKeywords.some(kw => c.includes(kw))) {
165
+ warnings.push({ file: r, type: "NO_GOOD_EXAMPLE", msg: "No correct example (✅) found" });
107
166
  }
108
- if (!c.includes("❌") && !c.includes("잘못된") && !c.includes("incorrect") && !c.includes("BAD")) {
109
- warnings.push({ file: r, type: "NO_BAD_EXAMPLE", msg: "잘못된 예시(❌) 없음" });
167
+ if (!badKeywords.some(kw => c.includes(kw))) {
168
+ warnings.push({ file: r, type: "NO_BAD_EXAMPLE", msg: "No incorrect example (❌) found" });
110
169
  }
111
- if (!c.includes("|") && !c.includes("규칙") && !c.includes("Rule") && !c.includes("table")) {
112
- warnings.push({ file: r, type: "NO_TABLE", msg: "규칙 요약 테이블이 없는 것으로 보임" });
170
+ if (!c.includes("|") && !c.includes("rule") && !c.includes("Rule") && !c.includes("table")) {
171
+ warnings.push({ file: r, type: "NO_TABLE", msg: "Rules summary table appears to be missing" });
172
+ }
173
+ // Kotlin code block check: core and backend standard files should contain ```kotlin blocks
174
+ if (detectedLanguage === "kotlin") {
175
+ const kotlinRelevantPaths = ["backend-api", "00.core/02.", "00.core/03.", "30.security-db"];
176
+ if (kotlinRelevantPaths.some(p => r.includes(p))) {
177
+ if (!c.includes("```kotlin") && !c.includes("```kt")) {
178
+ warnings.push({ file: r, type: "NO_KOTLIN_BLOCK", msg: "No ```kotlin code block found (expected for Kotlin project)" });
179
+ }
180
+ }
113
181
  }
114
182
  }
115
- console.log(` ${stdFiles.length} 파일 검사`);
183
+ console.log(` ${stdFiles.length} files checked`);
116
184
  } else {
117
- errors.push({ file: "claudeos-core/standard/", type: "MISSING", msg: "standard 디렉토리 없음" });
185
+ errors.push({ file: "claudeos-core/standard/", type: "MISSING", msg: "standard directory not found" });
118
186
  }
119
187
 
120
188
  // ─── 4. claudeos-core/skills/** ────────────────────────
@@ -125,17 +193,17 @@ async function main() {
125
193
  for (const f of skillFiles) {
126
194
  const c = fs.readFileSync(f, "utf-8");
127
195
  if (c.trim().length === 0) {
128
- errors.push({ file: rel(f), type: "EMPTY", msg: " 파일" });
196
+ errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
129
197
  }
130
198
  }
131
- // 오케스트레이터 존재 확인
199
+ // Check orchestrator existence
132
200
  const orchestrators = skillFiles.filter(f => f.includes("01.scaffold") || f.includes("MANIFEST"));
133
201
  if (orchestrators.length === 0) {
134
- warnings.push({ file: "claudeos-core/skills/", type: "NO_ORCHESTRATOR", msg: "오케스트레이터 또는 MANIFEST 없음" });
202
+ warnings.push({ file: "claudeos-core/skills/", type: "NO_ORCHESTRATOR", msg: "No orchestrator or MANIFEST found" });
135
203
  }
136
- console.log(` ${skillFiles.length} 파일 검사 (오케스트레이터 ${orchestrators.length})`);
204
+ console.log(` ${skillFiles.length} files checked (${orchestrators.length} orchestrators)`);
137
205
  } else {
138
- errors.push({ file: "claudeos-core/skills/", type: "MISSING", msg: "skills 디렉토리 없음" });
206
+ errors.push({ file: "claudeos-core/skills/", type: "MISSING", msg: "skills directory not found" });
139
207
  }
140
208
 
141
209
  // ─── 5. claudeos-core/guide/** ─────────────────────────
@@ -156,17 +224,17 @@ async function main() {
156
224
  const gp = path.join(GUIDE_DIR, g);
157
225
  checked++;
158
226
  if (!fs.existsSync(gp)) {
159
- errors.push({ file: `claudeos-core/guide/${g}`, type: "MISSING", msg: "가이드 파일 미생성" });
227
+ errors.push({ file: `claudeos-core/guide/${g}`, type: "MISSING", msg: "Guide file not generated" });
160
228
  } else {
161
229
  const c = fs.readFileSync(gp, "utf-8");
162
230
  if (c.trim().length === 0) {
163
- errors.push({ file: `claudeos-core/guide/${g}`, type: "EMPTY", msg: " 파일" });
231
+ errors.push({ file: `claudeos-core/guide/${g}`, type: "EMPTY", msg: "Empty file" });
164
232
  }
165
233
  }
166
234
  }
167
- console.log(` ${expectedGuides.length}개 예상 파일 중 ${expectedGuides.filter(g => fs.existsSync(path.join(GUIDE_DIR, g))).length} 존재`);
235
+ console.log(` ${expectedGuides.filter(g => fs.existsSync(path.join(GUIDE_DIR, g))).length} of ${expectedGuides.length} expected files exist`);
168
236
  } else {
169
- errors.push({ file: "claudeos-core/guide/", type: "MISSING", msg: "guide 디렉토리 없음" });
237
+ errors.push({ file: "claudeos-core/guide/", type: "MISSING", msg: "guide directory not found" });
170
238
  }
171
239
 
172
240
  // ─── 6. claudeos-core/plan/** ──────────────────────────
@@ -177,61 +245,61 @@ async function main() {
177
245
  checked++;
178
246
  const c = fs.readFileSync(f, "utf-8");
179
247
  if (c.trim().length === 0) {
180
- errors.push({ file: rel(f), type: "EMPTY", msg: " 플랜 파일" });
248
+ errors.push({ file: rel(f), type: "EMPTY", msg: "Empty plan file" });
181
249
  continue;
182
250
  }
183
- // <file> 블록이 있어야 함 (sync-rules-master 코드블록 방식)
251
+ // Must contain <file> blocks (sync-rules-master uses code block format)
184
252
  const bn = path.basename(f);
185
253
  if (!bn.includes("sync")) {
186
254
  if (!c.includes("<file") && !c.includes("```")) {
187
- warnings.push({ file: rel(f), type: "NO_FILE_BLOCKS", msg: "<file> 블록 또는 코드블록 없음" });
255
+ warnings.push({ file: rel(f), type: "NO_FILE_BLOCKS", msg: "No <file> blocks or code blocks found" });
188
256
  }
189
257
  }
190
258
  }
191
- console.log(` ${planFiles.length} 파일 검사`);
259
+ console.log(` ${planFiles.length} files checked`);
192
260
  } else {
193
- errors.push({ file: "claudeos-core/plan/", type: "MISSING", msg: "plan 디렉토리 없음" });
261
+ errors.push({ file: "claudeos-core/plan/", type: "MISSING", msg: "plan directory not found" });
194
262
  }
195
263
 
196
264
  // ─── 7. claudeos-core/database/** ──────────────────────
197
265
  console.log(" [7/8] claudeos-core/database/...");
198
266
  if (fs.existsSync(DB_DIR)) {
199
- const dbFiles = await glob("*.md", { cwd: DB_DIR, absolute: true });
267
+ const dbFiles = await glob("**/*.md", { cwd: DB_DIR, absolute: true });
200
268
  checked += dbFiles.length;
201
269
  if (dbFiles.length === 0) {
202
- warnings.push({ file: "claudeos-core/database/", type: "NO_FILES", msg: "database 파일 없음" });
270
+ warnings.push({ file: "claudeos-core/database/", type: "NO_FILES", msg: "No database files found" });
203
271
  }
204
272
  for (const f of dbFiles) {
205
273
  const c = fs.readFileSync(f, "utf-8");
206
274
  if (c.trim().length === 0) {
207
- errors.push({ file: rel(f), type: "EMPTY", msg: " 파일" });
275
+ errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
208
276
  }
209
277
  }
210
- console.log(` ${dbFiles.length} 파일`);
278
+ console.log(` ${dbFiles.length} files`);
211
279
  } else {
212
- warnings.push({ file: "claudeos-core/database/", type: "MISSING", msg: "database 디렉토리 없음" });
280
+ warnings.push({ file: "claudeos-core/database/", type: "MISSING", msg: "database directory not found" });
213
281
  }
214
282
 
215
283
  // ─── 8. claudeos-core/mcp-guide/** ─────────────────────
216
284
  console.log(" [8/8] claudeos-core/mcp-guide/...");
217
285
  if (fs.existsSync(MCP_DIR)) {
218
- const mcpFiles = await glob("*.md", { cwd: MCP_DIR, absolute: true });
286
+ const mcpFiles = await glob("**/*.md", { cwd: MCP_DIR, absolute: true });
219
287
  checked += mcpFiles.length;
220
288
  if (mcpFiles.length === 0) {
221
- warnings.push({ file: "claudeos-core/mcp-guide/", type: "NO_FILES", msg: "mcp-guide 파일 없음" });
289
+ warnings.push({ file: "claudeos-core/mcp-guide/", type: "NO_FILES", msg: "No mcp-guide files found" });
222
290
  }
223
291
  for (const f of mcpFiles) {
224
292
  const c = fs.readFileSync(f, "utf-8");
225
293
  if (c.trim().length === 0) {
226
- errors.push({ file: rel(f), type: "EMPTY", msg: " 파일" });
294
+ errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
227
295
  }
228
296
  }
229
- console.log(` ${mcpFiles.length} 파일`);
297
+ console.log(` ${mcpFiles.length} files`);
230
298
  } else {
231
- warnings.push({ file: "claudeos-core/mcp-guide/", type: "MISSING", msg: "mcp-guide 디렉토리 없음" });
299
+ warnings.push({ file: "claudeos-core/mcp-guide/", type: "MISSING", msg: "mcp-guide directory not found" });
232
300
  }
233
301
 
234
- // ─── 결과 출력 ─────────────────────────────────────────
302
+ // ─── Output results ─────────────────────────────────────────
235
303
  console.log(`\n Checked ${checked} files\n`);
236
304
  if (errors.length) {
237
305
  console.log(` ❌ ERRORS (${errors.length}):`);
@@ -247,10 +315,13 @@ async function main() {
247
315
  console.log(" ✅ All content validation passed\n");
248
316
  }
249
317
 
250
- // stale-report에 기록
318
+ // Record in stale-report
251
319
  if (fs.existsSync(GEN_DIR)) {
252
320
  const rp = path.join(GEN_DIR, "stale-report.json");
253
- const ex = fs.existsSync(rp) ? JSON.parse(fs.readFileSync(rp, "utf-8")) : {};
321
+ let ex = {};
322
+ if (fs.existsSync(rp)) {
323
+ try { ex = JSON.parse(fs.readFileSync(rp, "utf-8")); } catch { ex = {}; }
324
+ }
254
325
  ex.contentValidation = { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length, details: { errors, warnings } };
255
326
  ex.summary = { ...ex.summary, contentErrors: errors.length, contentWarnings: warnings.length };
256
327
  fs.writeFileSync(rp, JSON.stringify(ex, null, 2));