claudeos-core 1.7.0 → 2.0.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/CONTRIBUTING.md +92 -59
  3. package/README.de.md +465 -240
  4. package/README.es.md +446 -223
  5. package/README.fr.md +461 -238
  6. package/README.hi.md +485 -261
  7. package/README.ja.md +440 -235
  8. package/README.ko.md +244 -56
  9. package/README.md +215 -47
  10. package/README.ru.md +462 -238
  11. package/README.vi.md +454 -230
  12. package/README.zh-CN.md +476 -252
  13. package/bin/cli.js +144 -140
  14. package/bin/commands/init.js +550 -46
  15. package/bin/commands/memory.js +426 -0
  16. package/bin/lib/cli-utils.js +206 -143
  17. package/bootstrap.sh +81 -390
  18. package/content-validator/index.js +436 -340
  19. package/lib/expected-guides.js +23 -0
  20. package/lib/expected-outputs.js +91 -0
  21. package/lib/language-config.js +35 -0
  22. package/lib/memory-scaffold.js +1014 -0
  23. package/lib/plan-parser.js +153 -149
  24. package/lib/staged-rules.js +118 -0
  25. package/manifest-generator/index.js +176 -171
  26. package/package.json +1 -1
  27. package/pass-json-validator/index.js +337 -299
  28. package/pass-prompts/templates/common/pass3-footer.md +16 -0
  29. package/pass-prompts/templates/common/pass4.md +317 -0
  30. package/pass-prompts/templates/common/staging-override.md +26 -0
  31. package/pass-prompts/templates/python-flask/pass1.md +119 -0
  32. package/pass-prompts/templates/python-flask/pass2.md +85 -0
  33. package/pass-prompts/templates/python-flask/pass3.md +103 -0
  34. package/plan-installer/domain-grouper.js +2 -1
  35. package/plan-installer/prompt-generator.js +120 -96
  36. package/plan-installer/scanners/scan-frontend.js +219 -10
  37. package/plan-installer/scanners/scan-java.js +226 -223
  38. package/plan-installer/scanners/scan-python.js +21 -0
  39. package/sync-checker/index.js +133 -132
@@ -1,340 +1,436 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * ClaudeOS-Core — Content Validator
5
- *
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: ["all files"] frontmatter
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
- *
16
- * Usage: npx claudeos-core <cmd> or node claudeos-core-tools/content-validator/index.js
17
- */
18
-
19
- const fs = require("fs");
20
- const path = require("path");
21
- const { glob } = require("glob");
22
- const { updateStaleReport } = require("../lib/stale-report");
23
-
24
- const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
25
- const RULES_DIR = path.join(ROOT, ".claude/rules");
26
- const STANDARD_DIR = path.join(ROOT, "claudeos-core/standard");
27
- const SKILLS_DIR = path.join(ROOT, "claudeos-core/skills");
28
- const GUIDE_DIR = path.join(ROOT, "claudeos-core/guide");
29
- const PLAN_DIR = path.join(ROOT, "claudeos-core/plan");
30
- const DB_DIR = path.join(ROOT, "claudeos-core/database");
31
- const MCP_DIR = path.join(ROOT, "claudeos-core/mcp-guide");
32
- const GEN_DIR = path.join(ROOT, "claudeos-core/generated");
33
-
34
- function rel(p) { return path.relative(ROOT, p).replace(/\\/g, "/"); }
35
-
36
- async function main() {
37
- console.log("\n╔═══════════════════════════════════════╗");
38
- console.log("║ ClaudeOS-Core — Content Validator ║");
39
- console.log("╚═══════════════════════════════════════╝\n");
40
-
41
- const errors = [];
42
- const warnings = [];
43
- let checked = 0;
44
-
45
- // ─── Detect language and stack from project-analysis.json ────────
46
- let detectedLanguage = null;
47
- let outputLang = "en";
48
- const paPath = path.join(GEN_DIR, "project-analysis.json");
49
- if (fs.existsSync(paPath)) {
50
- try {
51
- const pa = JSON.parse(fs.readFileSync(paPath, "utf-8"));
52
- detectedLanguage = pa.stack?.language || null;
53
- outputLang = pa.lang || "en";
54
- } catch (_e) { /* ignore */ }
55
- }
56
-
57
- // Language-aware section keywords for CLAUDE.md validation
58
- const SECTION_KEYWORDS = {
59
- en: ["Role", "Build", "Run", "Standard", "Skills"],
60
- ko: ["역할", "빌드", "실행", "표준", "스킬"],
61
- "zh-CN": ["角色", "构建", "运行", "标准", "技能"],
62
- ja: ["役割", "ビルド", "実行", "標準", "スキル"],
63
- es: ["Rol", "Compilar", "Ejecutar", "Estándar", "Habilidades"],
64
- vi: ["Vai trò", "Build", "Chạy", "Tiêu chuẩn", "Kỹ năng"],
65
- hi: ["भूमिका", "बिल्ड", "रन", "मानक", "कौशल"],
66
- ru: ["Роль", "Сборка", "Запуск", "Стандарт", "Навыки"],
67
- fr: ["Rôle", "Build", "Exécuter", "Standard", "Compétences"],
68
- de: ["Rolle", "Build", "Ausführen", "Standard", "Fähigkeiten"],
69
- };
70
-
71
- // ─── 1. CLAUDE.md ──────────────────────────────────────
72
- console.log(" [1/8] CLAUDE.md...");
73
- const claudeMd = path.join(ROOT, "CLAUDE.md");
74
- if (!fs.existsSync(claudeMd)) {
75
- errors.push({ file: "CLAUDE.md", type: "MISSING", msg: "CLAUDE.md does not exist" });
76
- } else {
77
- checked++;
78
- const content = fs.readFileSync(claudeMd, "utf-8");
79
- if (content.trim().length < 100) {
80
- errors.push({ file: "CLAUDE.md", type: "EMPTY", msg: "CLAUDE.md content is too short (<100 chars)" });
81
- }
82
- // Check sections in both English (fallback) and output language
83
- const langKeywords = SECTION_KEYWORDS[outputLang] || SECTION_KEYWORDS.en;
84
- const enKeywords = SECTION_KEYWORDS.en;
85
- for (let i = 0; i < enKeywords.length; i++) {
86
- const candidates = [enKeywords[i], langKeywords[i]].filter(Boolean);
87
- const found = candidates.some(kw => {
88
- const re = new RegExp(`(^|#|\\s)${kw.replace(/[.*+?^${}()|\\[\]\\\\]/g, "\\$&")}`, "im");
89
- return re.test(content);
90
- });
91
- if (!found) {
92
- warnings.push({ file: "CLAUDE.md", type: "MISSING_SECTION", msg: `'${enKeywords[i]}' / '${langKeywords[i]}' section is missing` });
93
- }
94
- }
95
- }
96
-
97
- // ─── 2. .claude/rules/** ───────────────────────────────
98
- console.log(" [2/8] .claude/rules/...");
99
- if (fs.existsSync(RULES_DIR)) {
100
- const ruleFiles = await glob("**/*.md", { cwd: RULES_DIR, absolute: true });
101
- for (const f of ruleFiles) {
102
- checked++;
103
- const c = fs.readFileSync(f, "utf-8");
104
- const r = rel(f);
105
- if (c.trim().length === 0) {
106
- errors.push({ file: r, type: "EMPTY", msg: "Empty file" });
107
- continue;
108
- }
109
- // All rules must have paths: frontmatter (value varies by category — e.g. ["**/*"] for core/backend, scoped patterns for infra/sync)
110
- const hasFrontmatter = c.replace(/^\uFEFF/, "").startsWith("---");
111
- const hasPathsKey = c.includes("paths:");
112
- if (!hasFrontmatter) {
113
- warnings.push({ file: r, type: "NO_FRONTMATTER", msg: "Missing YAML frontmatter (---)" });
114
- } else if (!hasPathsKey) {
115
- warnings.push({ file: r, type: "NO_PATHS", msg: "Frontmatter exists but missing paths: key" });
116
- }
117
- }
118
- console.log(` ${ruleFiles.length} files checked`);
119
- } else {
120
- errors.push({ file: ".claude/rules/", type: "MISSING", msg: "rules directory not found" });
121
- }
122
-
123
- // ─── 3. claudeos-core/standard/** ─────────────────────
124
- console.log(" [3/8] claudeos-core/standard/...");
125
- if (fs.existsSync(STANDARD_DIR)) {
126
- const stdFiles = await glob("**/*.md", { cwd: STANDARD_DIR, absolute: true });
127
- for (const f of stdFiles) {
128
- checked++;
129
- const c = fs.readFileSync(f, "utf-8");
130
- const r = rel(f);
131
- if (c.trim().length === 0) {
132
- errors.push({ file: r, type: "EMPTY", msg: "Empty file" });
133
- continue;
134
- }
135
- if (c.length < 200) {
136
- warnings.push({ file: r, type: "TOO_SHORT", msg: `Content is short (${c.length} chars)` });
137
- }
138
- // Language-aware ✅/❌ example detection (all 10 supported languages)
139
- const goodKeywords = [
140
- "✅", "Correct", "correct", "GOOD",
141
- "올바른", // ko
142
- "正确", // zh-CN
143
- "正しい", // ja
144
- "Correcto", // es
145
- "Đúng", // vi
146
- "सही", // hi
147
- "Правильн", // ru
148
- "Correct", // fr (same as en)
149
- "Richtig", // de
150
- ];
151
- const badKeywords = [
152
- "", "Incorrect", "incorrect", "BAD",
153
- "잘못된", // ko
154
- "错误", // zh-CN
155
- "誤った", // ja
156
- "Incorrecto", // es
157
- "Sai", // vi
158
- "गलत", // hi
159
- "Неправильн", // ru
160
- "Incorrect", // fr (same as en)
161
- "Falsch", // de
162
- ];
163
- if (!goodKeywords.some(kw => c.includes(kw))) {
164
- warnings.push({ file: r, type: "NO_GOOD_EXAMPLE", msg: "No correct example (✅) found" });
165
- }
166
- if (!badKeywords.some(kw => c.includes(kw))) {
167
- warnings.push({ file: r, type: "NO_BAD_EXAMPLE", msg: "No incorrect example (❌) found" });
168
- }
169
- // Check for markdown table: at least one line with | col | col | pattern
170
- const hasMarkdownTable = /\|.+\|.+\|/.test(c);
171
- if (!hasMarkdownTable) {
172
- warnings.push({ file: r, type: "NO_TABLE", msg: "Rules summary table appears to be missing" });
173
- }
174
- // Kotlin code block check: backend standard files should contain ```kotlin blocks
175
- // (core files excluded for multi-stack projects where core may cover frontend too)
176
- if (detectedLanguage === "kotlin") {
177
- const kotlinRequiredPaths = ["backend-api", "30.security-db"];
178
- const kotlinOptionalPaths = ["00.core/02.", "00.core/03."];
179
- const isRequired = kotlinRequiredPaths.some(p => r.includes(p));
180
- const isOptional = kotlinOptionalPaths.some(p => r.includes(p));
181
- if (isRequired || isOptional) {
182
- if (!c.includes("```kotlin") && !c.includes("```kt")) {
183
- if (isRequired) {
184
- warnings.push({ file: r, type: "NO_KOTLIN_BLOCK", msg: "No ```kotlin code block found (expected for Kotlin project)" });
185
- }
186
- // optional paths: skip warning (core files may legitimately lack kotlin blocks in multi-stack)
187
- }
188
- }
189
- }
190
- }
191
- console.log(` ${stdFiles.length} files checked`);
192
- } else {
193
- errors.push({ file: "claudeos-core/standard/", type: "MISSING", msg: "standard directory not found" });
194
- }
195
-
196
- // ─── 4. claudeos-core/skills/** ────────────────────────
197
- console.log(" [4/8] claudeos-core/skills/...");
198
- if (fs.existsSync(SKILLS_DIR)) {
199
- const skillFiles = await glob("**/*.md", { cwd: SKILLS_DIR, absolute: true });
200
- checked += skillFiles.length;
201
- for (const f of skillFiles) {
202
- const c = fs.readFileSync(f, "utf-8");
203
- if (c.trim().length === 0) {
204
- errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
205
- }
206
- }
207
- // Check orchestrator existence
208
- const orchestrators = skillFiles.filter(f => f.includes("01.scaffold") || f.includes("MANIFEST"));
209
- if (orchestrators.length === 0) {
210
- warnings.push({ file: "claudeos-core/skills/", type: "NO_ORCHESTRATOR", msg: "No orchestrator or MANIFEST found" });
211
- }
212
- console.log(` ${skillFiles.length} files checked (${orchestrators.length} orchestrators)`);
213
- } else {
214
- errors.push({ file: "claudeos-core/skills/", type: "MISSING", msg: "skills directory not found" });
215
- }
216
-
217
- // ─── 5. claudeos-core/guide/** ─────────────────────────
218
- console.log(" [5/8] claudeos-core/guide/...");
219
- const expectedGuides = [
220
- "01.onboarding/01.overview.md",
221
- "01.onboarding/02.quickstart.md",
222
- "01.onboarding/03.glossary.md",
223
- "02.usage/01.faq.md",
224
- "02.usage/02.real-world-examples.md",
225
- "02.usage/03.do-and-dont.md",
226
- "03.troubleshooting/01.troubleshooting.md",
227
- "04.architecture/01.file-map.md",
228
- "04.architecture/02.pros-and-cons.md",
229
- ];
230
- if (fs.existsSync(GUIDE_DIR)) {
231
- for (const g of expectedGuides) {
232
- const gp = path.join(GUIDE_DIR, g);
233
- checked++;
234
- if (!fs.existsSync(gp)) {
235
- errors.push({ file: `claudeos-core/guide/${g}`, type: "MISSING", msg: "Guide file not generated" });
236
- } else {
237
- const c = fs.readFileSync(gp, "utf-8");
238
- if (c.trim().length === 0) {
239
- errors.push({ file: `claudeos-core/guide/${g}`, type: "EMPTY", msg: "Empty file" });
240
- }
241
- }
242
- }
243
- console.log(` ${expectedGuides.filter(g => fs.existsSync(path.join(GUIDE_DIR, g))).length} of ${expectedGuides.length} expected files exist`);
244
- } else {
245
- errors.push({ file: "claudeos-core/guide/", type: "MISSING", msg: "guide directory not found" });
246
- }
247
-
248
- // ─── 6. claudeos-core/plan/** ──────────────────────────
249
- console.log(" [6/8] claudeos-core/plan/...");
250
- if (fs.existsSync(PLAN_DIR)) {
251
- const planFiles = await glob("*.md", { cwd: PLAN_DIR, absolute: true });
252
- for (const f of planFiles) {
253
- checked++;
254
- const c = fs.readFileSync(f, "utf-8");
255
- if (c.trim().length === 0) {
256
- errors.push({ file: rel(f), type: "EMPTY", msg: "Empty plan file" });
257
- continue;
258
- }
259
- // Must contain <file> blocks (sync-rules-master uses code block format)
260
- const bn = path.basename(f);
261
- if (!bn.includes("sync")) {
262
- if (!c.includes("<file") && !c.includes("```")) {
263
- warnings.push({ file: rel(f), type: "NO_FILE_BLOCKS", msg: "No <file> blocks or code blocks found" });
264
- }
265
- }
266
- }
267
- console.log(` ${planFiles.length} files checked`);
268
- } else {
269
- errors.push({ file: "claudeos-core/plan/", type: "MISSING", msg: "plan directory not found" });
270
- }
271
-
272
- // ─── 7. claudeos-core/database/** ──────────────────────
273
- console.log(" [7/8] claudeos-core/database/...");
274
- if (fs.existsSync(DB_DIR)) {
275
- const dbFiles = await glob("**/*.md", { cwd: DB_DIR, absolute: true });
276
- checked += dbFiles.length;
277
- if (dbFiles.length === 0) {
278
- warnings.push({ file: "claudeos-core/database/", type: "NO_FILES", msg: "No database files found" });
279
- }
280
- for (const f of dbFiles) {
281
- const c = fs.readFileSync(f, "utf-8");
282
- if (c.trim().length === 0) {
283
- errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
284
- }
285
- }
286
- console.log(` ${dbFiles.length} files`);
287
- } else {
288
- warnings.push({ file: "claudeos-core/database/", type: "MISSING", msg: "database directory not found" });
289
- }
290
-
291
- // ─── 8. claudeos-core/mcp-guide/** ─────────────────────
292
- console.log(" [8/8] claudeos-core/mcp-guide/...");
293
- if (fs.existsSync(MCP_DIR)) {
294
- const mcpFiles = await glob("**/*.md", { cwd: MCP_DIR, absolute: true });
295
- checked += mcpFiles.length;
296
- if (mcpFiles.length === 0) {
297
- warnings.push({ file: "claudeos-core/mcp-guide/", type: "NO_FILES", msg: "No mcp-guide files found" });
298
- }
299
- for (const f of mcpFiles) {
300
- const c = fs.readFileSync(f, "utf-8");
301
- if (c.trim().length === 0) {
302
- errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
303
- }
304
- }
305
- console.log(` ${mcpFiles.length} files`);
306
- } else {
307
- warnings.push({ file: "claudeos-core/mcp-guide/", type: "MISSING", msg: "mcp-guide directory not found" });
308
- }
309
-
310
- // ─── Output results ─────────────────────────────────────────
311
- console.log(`\n Checked ${checked} files\n`);
312
- if (errors.length) {
313
- console.log(` ❌ ERRORS (${errors.length}):`);
314
- errors.forEach(e => console.log(` [${e.type}] ${e.file}: ${e.msg}`));
315
- console.log();
316
- }
317
- if (warnings.length) {
318
- console.log(` ⚠️ WARNINGS (${warnings.length}):`);
319
- warnings.forEach(w => console.log(` [${w.type}] ${w.file}: ${w.msg}`));
320
- console.log();
321
- }
322
- if (!errors.length && !warnings.length) {
323
- console.log(" ✅ All content validation passed\n");
324
- }
325
-
326
- // Record in stale-report
327
- updateStaleReport(GEN_DIR, "contentValidation",
328
- { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length, details: { errors, warnings } },
329
- { contentErrors: errors.length, contentWarnings: warnings.length }
330
- );
331
-
332
- console.log(` Total: ${errors.length} errors, ${warnings.length} warnings\n`);
333
- process.exit(errors.length > 0 ? 1 : 0);
334
- }
335
-
336
- if (require.main === module) {
337
- main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
338
- }
339
-
340
- module.exports = { main };
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ClaudeOS-Core — Content Validator
5
+ *
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: ["all files"] frontmatter
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
+ * - memory files follow expected entry structure
16
+ *
17
+ * Usage: npx claudeos-core <cmd> or node claudeos-core-tools/content-validator/index.js
18
+ */
19
+
20
+ const fs = require("fs");
21
+ const path = require("path");
22
+ const { glob } = require("glob");
23
+ const { updateStaleReport } = require("../lib/stale-report");
24
+ const { EXPECTED_GUIDE_FILES } = require("../lib/expected-guides");
25
+
26
+ const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
27
+ const RULES_DIR = path.join(ROOT, ".claude/rules");
28
+ const STANDARD_DIR = path.join(ROOT, "claudeos-core/standard");
29
+ const SKILLS_DIR = path.join(ROOT, "claudeos-core/skills");
30
+ const GUIDE_DIR = path.join(ROOT, "claudeos-core/guide");
31
+ const PLAN_DIR = path.join(ROOT, "claudeos-core/plan");
32
+ const DB_DIR = path.join(ROOT, "claudeos-core/database");
33
+ const MCP_DIR = path.join(ROOT, "claudeos-core/mcp-guide");
34
+ const MEMORY_DIR = path.join(ROOT, "claudeos-core/memory");
35
+ const GEN_DIR = path.join(ROOT, "claudeos-core/generated");
36
+
37
+ const EXPECTED_MEMORY = ["decision-log.md", "failure-patterns.md", "compaction.md", "auto-rule-update.md"];
38
+
39
+ function rel(p) { return path.relative(ROOT, p).replace(/\\/g, "/"); }
40
+
41
+ async function main() {
42
+ console.log("\n╔═══════════════════════════════════════╗");
43
+ console.log("║ ClaudeOS-Core Content Validator ║");
44
+ console.log("╚═══════════════════════════════════════╝\n");
45
+
46
+ const errors = [];
47
+ const warnings = [];
48
+ let checked = 0;
49
+
50
+ // ─── Detect language and stack from project-analysis.json ────────
51
+ let detectedLanguage = null;
52
+ let outputLang = "en";
53
+ const paPath = path.join(GEN_DIR, "project-analysis.json");
54
+ if (fs.existsSync(paPath)) {
55
+ try {
56
+ const pa = JSON.parse(fs.readFileSync(paPath, "utf-8"));
57
+ detectedLanguage = pa.stack?.language || null;
58
+ outputLang = pa.lang || "en";
59
+ } catch (_e) { /* ignore */ }
60
+ }
61
+
62
+ // Language-aware section keywords for CLAUDE.md validation
63
+ const SECTION_KEYWORDS = {
64
+ en: ["Role", "Build", "Run", "Standard", "Skills"],
65
+ ko: ["역할", "빌드", "실행", "표준", "스킬"],
66
+ "zh-CN": ["角色", "构建", "运行", "标准", "技能"],
67
+ ja: ["役割", "ビルド", "実行", "標準", "スキル"],
68
+ es: ["Rol", "Compilar", "Ejecutar", "Estándar", "Habilidades"],
69
+ vi: ["Vai trò", "Build", "Chạy", "Tiêu chuẩn", "Kỹ năng"],
70
+ hi: ["भूमिका", "बिल्ड", "रन", "मानक", "कौशल"],
71
+ ru: ["Роль", "Сборка", "Запуск", "Стандарт", "Навыки"],
72
+ fr: ["Rôle", "Build", "Exécuter", "Standard", "Compétences"],
73
+ de: ["Rolle", "Build", "Ausführen", "Standard", "Fähigkeiten"],
74
+ };
75
+
76
+ // ─── 1. CLAUDE.md ──────────────────────────────────────
77
+ console.log(" [1/9] CLAUDE.md...");
78
+ const claudeMd = path.join(ROOT, "CLAUDE.md");
79
+ if (!fs.existsSync(claudeMd)) {
80
+ errors.push({ file: "CLAUDE.md", type: "MISSING", msg: "CLAUDE.md does not exist" });
81
+ } else {
82
+ checked++;
83
+ const content = fs.readFileSync(claudeMd, "utf-8");
84
+ if (content.trim().length < 100) {
85
+ errors.push({ file: "CLAUDE.md", type: "EMPTY", msg: "CLAUDE.md content is too short (<100 chars)" });
86
+ }
87
+ // Check sections in both English (fallback) and output language
88
+ const langKeywords = SECTION_KEYWORDS[outputLang] || SECTION_KEYWORDS.en;
89
+ const enKeywords = SECTION_KEYWORDS.en;
90
+ for (let i = 0; i < enKeywords.length; i++) {
91
+ const candidates = [enKeywords[i], langKeywords[i]].filter(Boolean);
92
+ const found = candidates.some(kw => {
93
+ const re = new RegExp(`(^|#|\\s)${kw.replace(/[.*+?^${}()|\\[\]\\\\]/g, "\\$&")}`, "im");
94
+ return re.test(content);
95
+ });
96
+ if (!found) {
97
+ warnings.push({ file: "CLAUDE.md", type: "MISSING_SECTION", msg: `'${enKeywords[i]}' / '${langKeywords[i]}' section is missing` });
98
+ }
99
+ }
100
+ }
101
+
102
+ // ─── 2. .claude/rules/** ───────────────────────────────
103
+ console.log(" [2/9] .claude/rules/...");
104
+ if (fs.existsSync(RULES_DIR)) {
105
+ const ruleFiles = await glob("**/*.md", { cwd: RULES_DIR, absolute: true });
106
+ for (const f of ruleFiles) {
107
+ checked++;
108
+ const c = fs.readFileSync(f, "utf-8");
109
+ const r = rel(f);
110
+ if (c.trim().length === 0) {
111
+ errors.push({ file: r, type: "EMPTY", msg: "Empty file" });
112
+ continue;
113
+ }
114
+ // All rules must have paths: frontmatter (value varies by category — e.g. ["**/*"] for core/backend, scoped patterns for infra/sync)
115
+ const hasFrontmatter = c.replace(/^\uFEFF/, "").startsWith("---");
116
+ const hasPathsKey = c.includes("paths:");
117
+ if (!hasFrontmatter) {
118
+ warnings.push({ file: r, type: "NO_FRONTMATTER", msg: "Missing YAML frontmatter (---)" });
119
+ } else if (!hasPathsKey) {
120
+ warnings.push({ file: r, type: "NO_PATHS", msg: "Frontmatter exists but missing paths: key" });
121
+ }
122
+ }
123
+ console.log(` ${ruleFiles.length} files checked`);
124
+ } else {
125
+ errors.push({ file: ".claude/rules/", type: "MISSING", msg: "rules directory not found" });
126
+ }
127
+
128
+ // ─── 3. claudeos-core/standard/** ─────────────────────
129
+ console.log(" [3/9] claudeos-core/standard/...");
130
+ if (fs.existsSync(STANDARD_DIR)) {
131
+ const stdFiles = await glob("**/*.md", { cwd: STANDARD_DIR, absolute: true });
132
+ for (const f of stdFiles) {
133
+ checked++;
134
+ const c = fs.readFileSync(f, "utf-8");
135
+ const r = rel(f);
136
+ if (c.trim().length === 0) {
137
+ errors.push({ file: r, type: "EMPTY", msg: "Empty file" });
138
+ continue;
139
+ }
140
+ if (c.length < 200) {
141
+ warnings.push({ file: r, type: "TOO_SHORT", msg: `Content is short (${c.length} chars)` });
142
+ }
143
+ // Language-aware ✅/❌ example detection (all 10 supported languages)
144
+ const goodKeywords = [
145
+ "", "Correct", "correct", "GOOD",
146
+ "올바른", // ko
147
+ "正确", // zh-CN
148
+ "正しい", // ja
149
+ "Correcto", // es
150
+ "Đúng", // vi
151
+ "सही", // hi
152
+ "Правильн", // ru
153
+ "Correct", // fr (same as en)
154
+ "Richtig", // de
155
+ ];
156
+ const badKeywords = [
157
+ "", "Incorrect", "incorrect", "BAD",
158
+ "잘못된", // ko
159
+ "错误", // zh-CN
160
+ "誤った", // ja
161
+ "Incorrecto", // es
162
+ "Sai", // vi
163
+ "गलत", // hi
164
+ "Неправильн", // ru
165
+ "Incorrect", // fr (same as en)
166
+ "Falsch", // de
167
+ ];
168
+ if (!goodKeywords.some(kw => c.includes(kw))) {
169
+ warnings.push({ file: r, type: "NO_GOOD_EXAMPLE", msg: "No correct example (✅) found" });
170
+ }
171
+ if (!badKeywords.some(kw => c.includes(kw))) {
172
+ warnings.push({ file: r, type: "NO_BAD_EXAMPLE", msg: "No incorrect example (❌) found" });
173
+ }
174
+ // Check for markdown table: at least one line with | col | col | pattern
175
+ const hasMarkdownTable = /\|.+\|.+\|/.test(c);
176
+ if (!hasMarkdownTable) {
177
+ warnings.push({ file: r, type: "NO_TABLE", msg: "Rules summary table appears to be missing" });
178
+ }
179
+ // Kotlin code block check: backend standard files should contain ```kotlin blocks
180
+ // (core files excluded for multi-stack projects where core may cover frontend too)
181
+ if (detectedLanguage === "kotlin") {
182
+ const kotlinRequiredPaths = ["backend-api", "30.security-db"];
183
+ const kotlinOptionalPaths = ["00.core/02.", "00.core/03."];
184
+ const isRequired = kotlinRequiredPaths.some(p => r.includes(p));
185
+ const isOptional = kotlinOptionalPaths.some(p => r.includes(p));
186
+ if (isRequired || isOptional) {
187
+ if (!c.includes("```kotlin") && !c.includes("```kt")) {
188
+ if (isRequired) {
189
+ warnings.push({ file: r, type: "NO_KOTLIN_BLOCK", msg: "No ```kotlin code block found (expected for Kotlin project)" });
190
+ }
191
+ // optional paths: skip warning (core files may legitimately lack kotlin blocks in multi-stack)
192
+ }
193
+ }
194
+ }
195
+ }
196
+ console.log(` ${stdFiles.length} files checked`);
197
+ } else {
198
+ errors.push({ file: "claudeos-core/standard/", type: "MISSING", msg: "standard directory not found" });
199
+ }
200
+
201
+ // ─── 4. claudeos-core/skills/** ────────────────────────
202
+ console.log(" [4/9] claudeos-core/skills/...");
203
+ if (fs.existsSync(SKILLS_DIR)) {
204
+ const skillFiles = await glob("**/*.md", { cwd: SKILLS_DIR, absolute: true });
205
+ checked += skillFiles.length;
206
+ for (const f of skillFiles) {
207
+ const c = fs.readFileSync(f, "utf-8");
208
+ if (c.trim().length === 0) {
209
+ errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
210
+ }
211
+ }
212
+ // Check orchestrator existence
213
+ const orchestrators = skillFiles.filter(f => f.includes("01.scaffold") || f.includes("MANIFEST"));
214
+ if (orchestrators.length === 0) {
215
+ warnings.push({ file: "claudeos-core/skills/", type: "NO_ORCHESTRATOR", msg: "No orchestrator or MANIFEST found" });
216
+ }
217
+ console.log(` ${skillFiles.length} files checked (${orchestrators.length} orchestrators)`);
218
+ } else {
219
+ errors.push({ file: "claudeos-core/skills/", type: "MISSING", msg: "skills directory not found" });
220
+ }
221
+
222
+ // ─── 5. claudeos-core/guide/** ─────────────────────────
223
+ console.log(" [5/9] claudeos-core/guide/...");
224
+ const expectedGuides = EXPECTED_GUIDE_FILES;
225
+ if (fs.existsSync(GUIDE_DIR)) {
226
+ for (const g of expectedGuides) {
227
+ const gp = path.join(GUIDE_DIR, g);
228
+ checked++;
229
+ if (!fs.existsSync(gp)) {
230
+ errors.push({ file: `claudeos-core/guide/${g}`, type: "MISSING", msg: "Guide file not generated" });
231
+ } else {
232
+ const c = fs.readFileSync(gp, "utf-8");
233
+ if (c.trim().length === 0) {
234
+ errors.push({ file: `claudeos-core/guide/${g}`, type: "EMPTY", msg: "Empty file" });
235
+ }
236
+ }
237
+ }
238
+ console.log(` ${expectedGuides.filter(g => fs.existsSync(path.join(GUIDE_DIR, g))).length} of ${expectedGuides.length} expected files exist`);
239
+ } else {
240
+ errors.push({ file: "claudeos-core/guide/", type: "MISSING", msg: "guide directory not found" });
241
+ }
242
+
243
+ // ─── 6. claudeos-core/plan/** ──────────────────────────
244
+ console.log(" [6/9] claudeos-core/plan/...");
245
+ if (fs.existsSync(PLAN_DIR)) {
246
+ const planFiles = await glob("*.md", { cwd: PLAN_DIR, absolute: true });
247
+ for (const f of planFiles) {
248
+ checked++;
249
+ const c = fs.readFileSync(f, "utf-8");
250
+ if (c.trim().length === 0) {
251
+ errors.push({ file: rel(f), type: "EMPTY", msg: "Empty plan file" });
252
+ continue;
253
+ }
254
+ // Must contain <file> blocks (sync-rules-master uses code block format)
255
+ const bn = path.basename(f);
256
+ if (!bn.includes("sync")) {
257
+ if (!c.includes("<file") && !c.includes("```")) {
258
+ warnings.push({ file: rel(f), type: "NO_FILE_BLOCKS", msg: "No <file> blocks or code blocks found" });
259
+ }
260
+ }
261
+ }
262
+ console.log(` ${planFiles.length} files checked`);
263
+ } else {
264
+ errors.push({ file: "claudeos-core/plan/", type: "MISSING", msg: "plan directory not found" });
265
+ }
266
+
267
+ // ─── 7. claudeos-core/database/** ──────────────────────
268
+ console.log(" [7/9] claudeos-core/database/...");
269
+ if (fs.existsSync(DB_DIR)) {
270
+ const dbFiles = await glob("**/*.md", { cwd: DB_DIR, absolute: true });
271
+ checked += dbFiles.length;
272
+ if (dbFiles.length === 0) {
273
+ warnings.push({ file: "claudeos-core/database/", type: "NO_FILES", msg: "No database files found" });
274
+ }
275
+ for (const f of dbFiles) {
276
+ const c = fs.readFileSync(f, "utf-8");
277
+ if (c.trim().length === 0) {
278
+ errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
279
+ }
280
+ }
281
+ console.log(` ${dbFiles.length} files`);
282
+ } else {
283
+ warnings.push({ file: "claudeos-core/database/", type: "MISSING", msg: "database directory not found" });
284
+ }
285
+
286
+ // ─── 8. claudeos-core/mcp-guide/** ─────────────────────
287
+ console.log(" [8/9] claudeos-core/mcp-guide/...");
288
+ if (fs.existsSync(MCP_DIR)) {
289
+ const mcpFiles = await glob("**/*.md", { cwd: MCP_DIR, absolute: true });
290
+ checked += mcpFiles.length;
291
+ if (mcpFiles.length === 0) {
292
+ warnings.push({ file: "claudeos-core/mcp-guide/", type: "NO_FILES", msg: "No mcp-guide files found" });
293
+ }
294
+ for (const f of mcpFiles) {
295
+ const c = fs.readFileSync(f, "utf-8");
296
+ if (c.trim().length === 0) {
297
+ errors.push({ file: rel(f), type: "EMPTY", msg: "Empty file" });
298
+ }
299
+ }
300
+ console.log(` ${mcpFiles.length} files`);
301
+ } else {
302
+ warnings.push({ file: "claudeos-core/mcp-guide/", type: "MISSING", msg: "mcp-guide directory not found" });
303
+ }
304
+
305
+ // ─── 9. claudeos-core/memory/ (L4) ─────────────────────
306
+ console.log(" [9/9] claudeos-core/memory/...");
307
+ if (fs.existsSync(MEMORY_DIR)) {
308
+ for (const name of EXPECTED_MEMORY) {
309
+ const fp = path.join(MEMORY_DIR, name);
310
+ checked++;
311
+ if (!fs.existsSync(fp)) {
312
+ errors.push({ file: `claudeos-core/memory/${name}`, type: "MISSING", msg: "Memory file not scaffolded" });
313
+ continue;
314
+ }
315
+ const c = fs.readFileSync(fp, "utf-8");
316
+ if (c.trim().length === 0) {
317
+ errors.push({ file: `claudeos-core/memory/${name}`, type: "EMPTY", msg: "Empty memory file" });
318
+ continue;
319
+ }
320
+ if (c.trim().length < 50) {
321
+ warnings.push({ file: `claudeos-core/memory/${name}`, type: "TOO_SHORT", msg: `Memory file too short (${c.trim().length} chars)` });
322
+ }
323
+
324
+ // ─── Structural validation (v2 — prevents silent failures in memory CLI) ───
325
+ if (name === "decision-log.md") {
326
+ // Entries must start with `## YYYY-MM-DD — <title>` format when present.
327
+ // Empty (header-only) seed is allowed.
328
+ // Fence-aware: ignore `## ...` lines inside ```...``` / ~~~...~~~ so
329
+ // example markdown inside a decision's body text isn't flagged.
330
+ const lines = c.split("\n");
331
+ const entryHeadings = [];
332
+ let inFence = false;
333
+ const FENCE_RE = /^(```|~~~)/;
334
+ for (const line of lines) {
335
+ if (FENCE_RE.test(line)) { inFence = !inFence; continue; }
336
+ if (!inFence && /^##\s+.+$/.test(line)) entryHeadings.push(line);
337
+ }
338
+ for (const h of entryHeadings) {
339
+ if (!/^##\s+\d{4}-\d{2}-\d{2}/.test(h)) {
340
+ warnings.push({
341
+ file: `claudeos-core/memory/${name}`,
342
+ type: "MALFORMED_ENTRY",
343
+ msg: `Heading does not start with ISO date: ${h.slice(0, 60)}`,
344
+ });
345
+ }
346
+ }
347
+ } else if (name === "failure-patterns.md") {
348
+ // Each entry should have frequency + last seen + fix/solution fields.
349
+ // Parse entries and flag any that miss required fields (warning, not error).
350
+ // Fence-aware: ignore `## ...` lines inside ```...``` or ~~~...~~~
351
+ // so example markdown inside a Fix body is not treated as an entry.
352
+ const lines = c.split("\n");
353
+ let curId = null;
354
+ let curBody = [];
355
+ const entries = [];
356
+ let inFence = false;
357
+ const FENCE_RE = /^(```|~~~)/;
358
+ for (const line of lines) {
359
+ if (FENCE_RE.test(line)) inFence = !inFence;
360
+ if (!inFence && /^##\s+/.test(line)) {
361
+ if (curId !== null) entries.push({ id: curId, body: curBody.join("\n") });
362
+ curId = line.replace(/^##\s+/, "").trim();
363
+ curBody = [];
364
+ } else if (curId !== null) {
365
+ curBody.push(line);
366
+ }
367
+ }
368
+ if (curId !== null) entries.push({ id: curId, body: curBody.join("\n") });
369
+ for (const e of entries) {
370
+ // Accept both plain (`- frequency:`) and bold (`- **frequency**:`)
371
+ // markdown — the memory CLI's parseField matches both since v2.0.
372
+ const hasFreq = /\b(?:frequency|count)\*{0,2}\s*[:=]/i.test(e.body);
373
+ const hasLastSeen = /\blast\s*seen\*{0,2}\s*[:=]?/i.test(e.body) || /^\d{4}-\d{2}-\d{2}/.test(e.id);
374
+ // Fix/solution must appear as a field line, not just any word —
375
+ // otherwise a verbose line containing "fix" or "prefix" would
376
+ // falsely satisfy the check.
377
+ const hasFix = /^\s*-\s*\*{0,2}\s*(?:fix|solution)\*{0,2}\s*[:=]/im.test(e.body);
378
+ const missing = [];
379
+ if (!hasFreq) missing.push("frequency");
380
+ if (!hasLastSeen) missing.push("last seen");
381
+ if (!hasFix) missing.push("fix/solution");
382
+ if (missing.length > 0) {
383
+ warnings.push({
384
+ file: `claudeos-core/memory/${name}`,
385
+ type: "MALFORMED_ENTRY",
386
+ msg: `Entry "${e.id.slice(0, 40)}" missing: ${missing.join(", ")} (memory CLI may skip it)`,
387
+ });
388
+ }
389
+ }
390
+ } else if (name === "compaction.md") {
391
+ // CLI-parsed marker `## Last Compaction` must exist (memory compact looks for it).
392
+ if (!c.includes("## Last Compaction")) {
393
+ warnings.push({
394
+ file: `claudeos-core/memory/${name}`,
395
+ type: "MISSING_MARKER",
396
+ msg: "`## Last Compaction` section missing (memory compact will append instead of update)",
397
+ });
398
+ }
399
+ }
400
+ }
401
+ console.log(` ${EXPECTED_MEMORY.filter(n => fs.existsSync(path.join(MEMORY_DIR, n))).length} of ${EXPECTED_MEMORY.length} expected files exist`);
402
+ } else {
403
+ warnings.push({ file: "claudeos-core/memory/", type: "MISSING", msg: "memory directory not found (run pass 4)" });
404
+ }
405
+
406
+ // ─── Output results ─────────────────────────────────────────
407
+ console.log(`\n Checked ${checked} files\n`);
408
+ if (errors.length) {
409
+ console.log(` ❌ ERRORS (${errors.length}):`);
410
+ errors.forEach(e => console.log(` [${e.type}] ${e.file}: ${e.msg}`));
411
+ console.log();
412
+ }
413
+ if (warnings.length) {
414
+ console.log(` ⚠️ WARNINGS (${warnings.length}):`);
415
+ warnings.forEach(w => console.log(` [${w.type}] ${w.file}: ${w.msg}`));
416
+ console.log();
417
+ }
418
+ if (!errors.length && !warnings.length) {
419
+ console.log(" ✅ All content validation passed\n");
420
+ }
421
+
422
+ // Record in stale-report
423
+ updateStaleReport(GEN_DIR, "contentValidation",
424
+ { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length, details: { errors, warnings } },
425
+ { contentErrors: errors.length, contentWarnings: warnings.length }
426
+ );
427
+
428
+ console.log(` Total: ${errors.length} errors, ${warnings.length} warnings\n`);
429
+ process.exit(errors.length > 0 ? 1 : 0);
430
+ }
431
+
432
+ if (require.main === module) {
433
+ main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
434
+ }
435
+
436
+ module.exports = { main };