claudeos-core 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +1664 -907
  2. package/CONTRIBUTING.md +92 -92
  3. package/README.de.md +28 -0
  4. package/README.es.md +28 -0
  5. package/README.fr.md +28 -0
  6. package/README.hi.md +28 -0
  7. package/README.ja.md +28 -0
  8. package/README.ko.md +1014 -986
  9. package/README.md +1016 -987
  10. package/README.ru.md +28 -0
  11. package/README.vi.md +1015 -987
  12. package/README.zh-CN.md +28 -0
  13. package/bin/cli.js +152 -148
  14. package/bin/commands/init.js +1673 -1554
  15. package/bin/commands/lint.js +62 -0
  16. package/bin/commands/memory.js +438 -438
  17. package/bin/lib/cli-utils.js +206 -206
  18. package/claude-md-validator/index.js +184 -0
  19. package/claude-md-validator/reporter.js +66 -0
  20. package/claude-md-validator/structural-checks.js +528 -0
  21. package/content-validator/index.js +666 -441
  22. package/lib/expected-guides.js +23 -23
  23. package/lib/expected-outputs.js +90 -90
  24. package/lib/language-config.js +35 -35
  25. package/lib/memory-scaffold.js +1058 -1054
  26. package/lib/plan-parser.js +165 -165
  27. package/lib/staged-rules.js +118 -118
  28. package/manifest-generator/index.js +174 -174
  29. package/package.json +90 -87
  30. package/pass-json-validator/index.js +337 -337
  31. package/pass-prompts/templates/common/claude-md-scaffold.md +52 -10
  32. package/pass-prompts/templates/common/pass3-footer.md +402 -224
  33. package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
  34. package/pass-prompts/templates/common/pass4.md +375 -305
  35. package/pass-prompts/templates/common/staging-override.md +26 -26
  36. package/pass-prompts/templates/node-vite/pass1.md +117 -117
  37. package/pass-prompts/templates/node-vite/pass2.md +78 -78
  38. package/pass-prompts/templates/python-flask/pass1.md +119 -119
  39. package/pass-prompts/templates/python-flask/pass2.md +85 -85
  40. package/plan-installer/domain-grouper.js +76 -76
  41. package/plan-installer/index.js +137 -137
  42. package/plan-installer/prompt-generator.js +188 -145
  43. package/plan-installer/scanners/scan-frontend.js +505 -473
  44. package/plan-installer/scanners/scan-java.js +226 -226
  45. package/plan-installer/scanners/scan-node.js +57 -57
  46. package/plan-installer/scanners/scan-python.js +85 -85
  47. package/plan-installer/stack-detector.js +482 -482
  48. package/plan-installer/structure-scanner.js +65 -65
  49. package/sync-checker/index.js +177 -177
@@ -1,441 +1,666 @@
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
- // v2.1.0+ removed master plan generation; plan/ is optional and is not created
245
- // during fresh init. If the directory exists (legacy projects, user-authored
246
- // plan files), we still validate its contents. If it is absent, that is the
247
- // expected state post-v2.1.0 — do not push a MISSING error (parallel to
248
- // plan-validator / manifest-generator which were already updated in v2.1.0).
249
- console.log(" [6/9] 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
- console.log(" ⏭️ plan/ not present (expected post-v2.1.0)");
270
- }
271
-
272
- // ─── 7. claudeos-core/database/** ──────────────────────
273
- console.log(" [7/9] 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/9] 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
- // ─── 9. claudeos-core/memory/ (L4) ─────────────────────
311
- console.log(" [9/9] claudeos-core/memory/...");
312
- if (fs.existsSync(MEMORY_DIR)) {
313
- for (const name of EXPECTED_MEMORY) {
314
- const fp = path.join(MEMORY_DIR, name);
315
- checked++;
316
- if (!fs.existsSync(fp)) {
317
- errors.push({ file: `claudeos-core/memory/${name}`, type: "MISSING", msg: "Memory file not scaffolded" });
318
- continue;
319
- }
320
- const c = fs.readFileSync(fp, "utf-8");
321
- if (c.trim().length === 0) {
322
- errors.push({ file: `claudeos-core/memory/${name}`, type: "EMPTY", msg: "Empty memory file" });
323
- continue;
324
- }
325
- if (c.trim().length < 50) {
326
- warnings.push({ file: `claudeos-core/memory/${name}`, type: "TOO_SHORT", msg: `Memory file too short (${c.trim().length} chars)` });
327
- }
328
-
329
- // ─── Structural validation (v2 — prevents silent failures in memory CLI) ───
330
- if (name === "decision-log.md") {
331
- // Entries must start with `## YYYY-MM-DD — <title>` format when present.
332
- // Empty (header-only) seed is allowed.
333
- // Fence-aware: ignore `## ...` lines inside ```...``` / ~~~...~~~ so
334
- // example markdown inside a decision's body text isn't flagged.
335
- const lines = c.split("\n");
336
- const entryHeadings = [];
337
- let inFence = false;
338
- const FENCE_RE = /^(```|~~~)/;
339
- for (const line of lines) {
340
- if (FENCE_RE.test(line)) { inFence = !inFence; continue; }
341
- if (!inFence && /^##\s+.+$/.test(line)) entryHeadings.push(line);
342
- }
343
- for (const h of entryHeadings) {
344
- if (!/^##\s+\d{4}-\d{2}-\d{2}/.test(h)) {
345
- warnings.push({
346
- file: `claudeos-core/memory/${name}`,
347
- type: "MALFORMED_ENTRY",
348
- msg: `Heading does not start with ISO date: ${h.slice(0, 60)}`,
349
- });
350
- }
351
- }
352
- } else if (name === "failure-patterns.md") {
353
- // Each entry should have frequency + last seen + fix/solution fields.
354
- // Parse entries and flag any that miss required fields (warning, not error).
355
- // Fence-aware: ignore `## ...` lines inside ```...``` or ~~~...~~~
356
- // so example markdown inside a Fix body is not treated as an entry.
357
- const lines = c.split("\n");
358
- let curId = null;
359
- let curBody = [];
360
- const entries = [];
361
- let inFence = false;
362
- const FENCE_RE = /^(```|~~~)/;
363
- for (const line of lines) {
364
- if (FENCE_RE.test(line)) inFence = !inFence;
365
- if (!inFence && /^##\s+/.test(line)) {
366
- if (curId !== null) entries.push({ id: curId, body: curBody.join("\n") });
367
- curId = line.replace(/^##\s+/, "").trim();
368
- curBody = [];
369
- } else if (curId !== null) {
370
- curBody.push(line);
371
- }
372
- }
373
- if (curId !== null) entries.push({ id: curId, body: curBody.join("\n") });
374
- for (const e of entries) {
375
- // Accept both plain (`- frequency:`) and bold (`- **frequency**:`)
376
- // markdown — the memory CLI's parseField matches both since v2.0.
377
- const hasFreq = /\b(?:frequency|count)\*{0,2}\s*[:=]/i.test(e.body);
378
- const hasLastSeen = /\blast\s*seen\*{0,2}\s*[:=]?/i.test(e.body) || /^\d{4}-\d{2}-\d{2}/.test(e.id);
379
- // Fix/solution must appear as a field line, not just any word —
380
- // otherwise a verbose line containing "fix" or "prefix" would
381
- // falsely satisfy the check.
382
- const hasFix = /^\s*-\s*\*{0,2}\s*(?:fix|solution)\*{0,2}\s*[:=]/im.test(e.body);
383
- const missing = [];
384
- if (!hasFreq) missing.push("frequency");
385
- if (!hasLastSeen) missing.push("last seen");
386
- if (!hasFix) missing.push("fix/solution");
387
- if (missing.length > 0) {
388
- warnings.push({
389
- file: `claudeos-core/memory/${name}`,
390
- type: "MALFORMED_ENTRY",
391
- msg: `Entry "${e.id.slice(0, 40)}" missing: ${missing.join(", ")} (memory CLI may skip it)`,
392
- });
393
- }
394
- }
395
- } else if (name === "compaction.md") {
396
- // CLI-parsed marker `## Last Compaction` must exist (memory compact looks for it).
397
- if (!c.includes("## Last Compaction")) {
398
- warnings.push({
399
- file: `claudeos-core/memory/${name}`,
400
- type: "MISSING_MARKER",
401
- msg: "`## Last Compaction` section missing (memory compact will append instead of update)",
402
- });
403
- }
404
- }
405
- }
406
- console.log(` ${EXPECTED_MEMORY.filter(n => fs.existsSync(path.join(MEMORY_DIR, n))).length} of ${EXPECTED_MEMORY.length} expected files exist`);
407
- } else {
408
- warnings.push({ file: "claudeos-core/memory/", type: "MISSING", msg: "memory directory not found (run pass 4)" });
409
- }
410
-
411
- // ─── Output results ─────────────────────────────────────────
412
- console.log(`\n Checked ${checked} files\n`);
413
- if (errors.length) {
414
- console.log(` ERRORS (${errors.length}):`);
415
- errors.forEach(e => console.log(` [${e.type}] ${e.file}: ${e.msg}`));
416
- console.log();
417
- }
418
- if (warnings.length) {
419
- console.log(` ⚠️ WARNINGS (${warnings.length}):`);
420
- warnings.forEach(w => console.log(` [${w.type}] ${w.file}: ${w.msg}`));
421
- console.log();
422
- }
423
- if (!errors.length && !warnings.length) {
424
- console.log(" All content validation passed\n");
425
- }
426
-
427
- // Record in stale-report
428
- updateStaleReport(GEN_DIR, "contentValidation",
429
- { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length, details: { errors, warnings } },
430
- { contentErrors: errors.length, contentWarnings: warnings.length }
431
- );
432
-
433
- console.log(` Total: ${errors.length} errors, ${warnings.length} warnings\n`);
434
- process.exit(errors.length > 0 ? 1 : 0);
435
- }
436
-
437
- if (require.main === module) {
438
- main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
439
- }
440
-
441
- 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
+ // v2.1.0+ removed master plan generation; plan/ is optional and is not created
245
+ // during fresh init. If the directory exists (legacy projects, user-authored
246
+ // plan files), we still validate its contents. If it is absent, that is the
247
+ // expected state post-v2.1.0 — do not push a MISSING error (parallel to
248
+ // plan-validator / manifest-generator which were already updated in v2.1.0).
249
+ console.log(" [6/9] 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
+ console.log(" ⏭️ plan/ not present (expected post-v2.1.0)");
270
+ }
271
+
272
+ // ─── 7. claudeos-core/database/** ──────────────────────
273
+ console.log(" [7/9] 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/9] 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
+ // ─── 9. claudeos-core/memory/ (L4) ─────────────────────
311
+ console.log(" [9/9] claudeos-core/memory/...");
312
+ if (fs.existsSync(MEMORY_DIR)) {
313
+ for (const name of EXPECTED_MEMORY) {
314
+ const fp = path.join(MEMORY_DIR, name);
315
+ checked++;
316
+ if (!fs.existsSync(fp)) {
317
+ errors.push({ file: `claudeos-core/memory/${name}`, type: "MISSING", msg: "Memory file not scaffolded" });
318
+ continue;
319
+ }
320
+ const c = fs.readFileSync(fp, "utf-8");
321
+ if (c.trim().length === 0) {
322
+ errors.push({ file: `claudeos-core/memory/${name}`, type: "EMPTY", msg: "Empty memory file" });
323
+ continue;
324
+ }
325
+ if (c.trim().length < 50) {
326
+ warnings.push({ file: `claudeos-core/memory/${name}`, type: "TOO_SHORT", msg: `Memory file too short (${c.trim().length} chars)` });
327
+ }
328
+
329
+ // ─── Structural validation (v2 — prevents silent failures in memory CLI) ───
330
+ if (name === "decision-log.md") {
331
+ // Entries must start with `## YYYY-MM-DD — <title>` format when present.
332
+ // Empty (header-only) seed is allowed.
333
+ // Fence-aware: ignore `## ...` lines inside ```...``` / ~~~...~~~ so
334
+ // example markdown inside a decision's body text isn't flagged.
335
+ const lines = c.split("\n");
336
+ const entryHeadings = [];
337
+ let inFence = false;
338
+ const FENCE_RE = /^(```|~~~)/;
339
+ for (const line of lines) {
340
+ if (FENCE_RE.test(line)) { inFence = !inFence; continue; }
341
+ if (!inFence && /^##\s+.+$/.test(line)) entryHeadings.push(line);
342
+ }
343
+ for (const h of entryHeadings) {
344
+ if (!/^##\s+\d{4}-\d{2}-\d{2}/.test(h)) {
345
+ warnings.push({
346
+ file: `claudeos-core/memory/${name}`,
347
+ type: "MALFORMED_ENTRY",
348
+ msg: `Heading does not start with ISO date: ${h.slice(0, 60)}`,
349
+ });
350
+ }
351
+ }
352
+ } else if (name === "failure-patterns.md") {
353
+ // Each entry should have frequency + last seen + fix/solution fields.
354
+ // Parse entries and flag any that miss required fields (warning, not error).
355
+ // Fence-aware: ignore `## ...` lines inside ```...``` or ~~~...~~~
356
+ // so example markdown inside a Fix body is not treated as an entry.
357
+ const lines = c.split("\n");
358
+ let curId = null;
359
+ let curBody = [];
360
+ const entries = [];
361
+ let inFence = false;
362
+ const FENCE_RE = /^(```|~~~)/;
363
+ for (const line of lines) {
364
+ if (FENCE_RE.test(line)) inFence = !inFence;
365
+ if (!inFence && /^##\s+/.test(line)) {
366
+ if (curId !== null) entries.push({ id: curId, body: curBody.join("\n") });
367
+ curId = line.replace(/^##\s+/, "").trim();
368
+ curBody = [];
369
+ } else if (curId !== null) {
370
+ curBody.push(line);
371
+ }
372
+ }
373
+ if (curId !== null) entries.push({ id: curId, body: curBody.join("\n") });
374
+ for (const e of entries) {
375
+ // Accept both plain (`- frequency:`) and bold (`- **frequency**:`)
376
+ // markdown — the memory CLI's parseField matches both since v2.0.
377
+ const hasFreq = /\b(?:frequency|count)\*{0,2}\s*[:=]/i.test(e.body);
378
+ const hasLastSeen = /\blast\s*seen\*{0,2}\s*[:=]?/i.test(e.body) || /^\d{4}-\d{2}-\d{2}/.test(e.id);
379
+ // Fix/solution must appear as a field line, not just any word —
380
+ // otherwise a verbose line containing "fix" or "prefix" would
381
+ // falsely satisfy the check.
382
+ const hasFix = /^\s*-\s*\*{0,2}\s*(?:fix|solution)\*{0,2}\s*[:=]/im.test(e.body);
383
+ const missing = [];
384
+ if (!hasFreq) missing.push("frequency");
385
+ if (!hasLastSeen) missing.push("last seen");
386
+ if (!hasFix) missing.push("fix/solution");
387
+ if (missing.length > 0) {
388
+ warnings.push({
389
+ file: `claudeos-core/memory/${name}`,
390
+ type: "MALFORMED_ENTRY",
391
+ msg: `Entry "${e.id.slice(0, 40)}" missing: ${missing.join(", ")} (memory CLI may skip it)`,
392
+ });
393
+ }
394
+ }
395
+ } else if (name === "compaction.md") {
396
+ // CLI-parsed marker `## Last Compaction` must exist (memory compact looks for it).
397
+ if (!c.includes("## Last Compaction")) {
398
+ warnings.push({
399
+ file: `claudeos-core/memory/${name}`,
400
+ type: "MISSING_MARKER",
401
+ msg: "`## Last Compaction` section missing (memory compact will append instead of update)",
402
+ });
403
+ }
404
+ }
405
+ }
406
+ console.log(` ${EXPECTED_MEMORY.filter(n => fs.existsSync(path.join(MEMORY_DIR, n))).length} of ${EXPECTED_MEMORY.length} expected files exist`);
407
+ } else {
408
+ warnings.push({ file: "claudeos-core/memory/", type: "MISSING", msg: "memory directory not found (run pass 4)" });
409
+ }
410
+
411
+ // ─── 10. Path-claim verification ──────────────────────────
412
+ // Catches two dogfood-surfaced failure classes:
413
+ // (a) Pass 3 hallucinations: rules/standard files reference
414
+ // src/... paths the LLM fabricated from directory context
415
+ // (e.g., `src/feature/routers/featureRoutePath.ts` when the actual
416
+ // file is `src/feature/routers/routePath.ts` — "feature" came from
417
+ // the parent dir, not the filename).
418
+ // (b) MANIFEST ↔ CLAUDE.md §6 Skills drift: a skill is registered
419
+ // in claudeos-core/skills/00.shared/MANIFEST.md but missing
420
+ // from CLAUDE.md §6 Skills list (or vice versa).
421
+ //
422
+ // Both manifest as a stale path reference, so a single structural
423
+ // check covers both. No natural-language matching involved.
424
+ console.log(" [10/10] path-claim verification (hallucination + MANIFEST drift)...");
425
+
426
+ // Regex: matches `src/...` paths to TS/TSX/JS/JSX files, not inside
427
+ // inline code already fenced. We still strip fenced blocks first so
428
+ // example blocks inside ```...``` don't produce false positives.
429
+ const SRC_PATH_RE = /\bsrc\/[\w\-./]+\.(?:ts|tsx|js|jsx)\b/g;
430
+ // Placeholder paths like `src/{domain}/...` are scaffold templates,
431
+ // not real path claims. Skip them.
432
+ const hasPlaceholder = (p) => /\{[^}]+\}/.test(p);
433
+
434
+ // Strip fenced code blocks (``` and ~~~) so examples inside code
435
+ // blocks don't trigger the check — they're illustrations, not claims.
436
+ function stripFences(text) {
437
+ const lines = text.split(/\r?\n/);
438
+ let inFence = false;
439
+ let marker = null;
440
+ const out = [];
441
+ for (const line of lines) {
442
+ const t = line.trimStart();
443
+ const m = t.match(/^(```+|~~~+)/);
444
+ if (m) {
445
+ if (!inFence) { inFence = true; marker = m[1][0]; }
446
+ else if (t.startsWith(marker)) { inFence = false; marker = null; }
447
+ out.push(""); // preserve line count but blank the fence markers
448
+ continue;
449
+ }
450
+ out.push(inFence ? "" : line);
451
+ }
452
+ return out.join("\n");
453
+ }
454
+
455
+ // Scan rules/ and standard/ for src/... path claims.
456
+ const pathClaimTargets = [
457
+ { label: "rules", dir: RULES_DIR, glob: "**/*.md" },
458
+ { label: "standard", dir: STANDARD_DIR, glob: "**/*.md" },
459
+ ];
460
+ let pathClaimsChecked = 0;
461
+ let pathClaimErrors = 0;
462
+ for (const target of pathClaimTargets) {
463
+ if (!fs.existsSync(target.dir)) continue;
464
+ const files = await glob(target.glob, { cwd: target.dir, absolute: true });
465
+ for (const file of files) {
466
+ const raw = fs.readFileSync(file, "utf-8");
467
+ const stripped = stripFences(raw);
468
+ const seen = new Set(); // dedupe within a single file
469
+ let m;
470
+ SRC_PATH_RE.lastIndex = 0;
471
+ while ((m = SRC_PATH_RE.exec(stripped)) !== null) {
472
+ const claimed = m[0];
473
+ if (seen.has(claimed)) continue;
474
+ seen.add(claimed);
475
+ if (hasPlaceholder(claimed)) continue;
476
+ pathClaimsChecked++;
477
+ const absolutePath = path.join(ROOT, claimed);
478
+ if (!fs.existsSync(absolutePath)) {
479
+ pathClaimErrors++;
480
+ errors.push({
481
+ file: rel(file),
482
+ type: "STALE_PATH",
483
+ msg: `References "${claimed}" which does not exist in the repository. ` +
484
+ `Likely Pass 3 hallucination — re-run with \`init --force\` after ` +
485
+ `verifying the correct path in pass2-merged.json.`,
486
+ });
487
+ }
488
+ }
489
+ }
490
+ }
491
+ console.log(` ${pathClaimsChecked} path claim(s) checked, ${pathClaimErrors} stale`);
492
+
493
+ // MANIFEST ↔ CLAUDE.md §6 Skills drift check.
494
+ // MANIFEST registers skills in a 4-column table; each row's second
495
+ // cell contains a backtick-wrapped path to the skill's entry file.
496
+ // CLAUDE.md §6 lists skills under a `### Skills` sub-section (title
497
+ // localized, but structure is sub-section inside §6).
498
+ //
499
+ // We detect drift in BOTH directions:
500
+ // - MANIFEST entry path does not exist on disk → STALE_SKILL_ENTRY
501
+ // - MANIFEST entry not referenced in CLAUDE.md §6 → MANIFEST_DRIFT
502
+ const manifestPath = path.join(SKILLS_DIR, "00.shared", "MANIFEST.md");
503
+ let manifestErrors = 0;
504
+ if (fs.existsSync(manifestPath)) {
505
+ const manifest = fs.readFileSync(manifestPath, "utf-8");
506
+ const stripped = stripFences(manifest);
507
+
508
+ // Pull every `claudeos-core/skills/...` path that appears inside
509
+ // a backtick span in the MANIFEST. This catches the table's
510
+ // "entry" column regardless of the heading language ("등록된
511
+ // 스킬" / "Registered Skills" / "登録済みスキル" — all match).
512
+ const SKILL_PATH_RE = /`(claudeos-core\/skills\/[\w\-./]+\.md)`/g;
513
+ const registered = new Set();
514
+ let m;
515
+ while ((m = SKILL_PATH_RE.exec(stripped)) !== null) {
516
+ // Skip MANIFEST.md itself — it's always self-referenced but is
517
+ // a meta-file, not a skill.
518
+ if (m[1].endsWith("/MANIFEST.md")) continue;
519
+ registered.add(m[1]);
520
+ }
521
+
522
+ // Stage 1: each registered skill path must exist on disk.
523
+ for (const p of registered) {
524
+ const abs = path.join(ROOT, p);
525
+ if (!fs.existsSync(abs)) {
526
+ manifestErrors++;
527
+ errors.push({
528
+ file: "claudeos-core/skills/00.shared/MANIFEST.md",
529
+ type: "STALE_SKILL_ENTRY",
530
+ msg: `Registered skill "${p}" does not exist on disk. ` +
531
+ `Either create the skill file or remove the MANIFEST row.`,
532
+ });
533
+ }
534
+ }
535
+
536
+ // Stage 2: MANIFEST ↔ CLAUDE.md §6 cross-reference.
537
+ const claudeMdPathForSync = path.join(ROOT, "CLAUDE.md");
538
+ if (fs.existsSync(claudeMdPathForSync)) {
539
+ const claudeMd = fs.readFileSync(claudeMdPathForSync, "utf-8");
540
+ const mdStripped = stripFences(claudeMd);
541
+ // Extract every skill path referenced in the whole CLAUDE.md
542
+ // body. We intentionally don't try to scope to §6 alone — any
543
+ // registered skill mentioned anywhere is considered "referenced"
544
+ // and avoids false positives for alternate layouts.
545
+ const referenced = new Set();
546
+ SKILL_PATH_RE.lastIndex = 0;
547
+ while ((m = SKILL_PATH_RE.exec(mdStripped)) !== null) {
548
+ if (m[1].endsWith("/MANIFEST.md")) continue;
549
+ referenced.add(m[1]);
550
+ }
551
+
552
+ // v2.3.0 — Sub-skill exception for the orchestrator/sub-skill
553
+ // pattern. Rationale:
554
+ //
555
+ // Skills commonly ship as:
556
+ // skills/{category}/{NN}.{name}.md ← orchestrator
557
+ // skills/{category}/{name}/{NN}.{step}.md ← sub-skills
558
+ //
559
+ // (Example: 10.backend-crud/01.scaffold-crud-feature.md plus
560
+ // 10.backend-crud/scaffold-crud-feature/01.dto.md, 02.mapper.md, …)
561
+ //
562
+ // Structurally, Pass 3b writes CLAUDE.md §6 before Pass 3c creates
563
+ // the skills + MANIFEST. Pass 3b cannot list every sub-skill by
564
+ // name because they don't exist yet, and having it predict the
565
+ // full list produces filename hallucinations (02.entity.md when
566
+ // Pass 3c actually emits 02.mapper.md, etc.).
567
+ //
568
+ // The correct design is role-separated: CLAUDE.md §6 is an entry
569
+ // point that names categories and orchestrators; MANIFEST.md is
570
+ // the authoritative registry for sub-skill details. So when the
571
+ // orchestrator for a sub-skill is referenced anywhere in CLAUDE.md,
572
+ // we treat its sub-skills as covered transitively through the
573
+ // orchestrator row/MANIFEST indirection, and suppress
574
+ // MANIFEST_DRIFT for them. STALE_SKILL_ENTRY (sub-skill registered
575
+ // but file missing) still fires in Stage 1 above — that's a real
576
+ // MANIFEST integrity issue and is not subject to this relaxation.
577
+ //
578
+ // A sub-skill is identified by a trailing `{parent}/{NN}.{name}.md`
579
+ // segment whose `{parent}` directory sits one level below
580
+ // `skills/{category}/`. The orchestrator then lives at
581
+ // `skills/{category}/{NN-or-something}.{parent}.md`. We don't
582
+ // require a specific numeric prefix on the orchestrator — any
583
+ // file of the form `skills/{category}/*{parent}*.md` (excluding
584
+ // the sub-skill itself) counts as a plausible orchestrator.
585
+ function orchestratorFor(subSkillPath) {
586
+ const m = subSkillPath.match(
587
+ /^(claudeos-core\/skills\/[^/]+\/)([^/]+)\/\d+\.[^/]+\.md$/
588
+ );
589
+ if (!m) return null;
590
+ return { categoryDir: m[1], stem: m[2] };
591
+ }
592
+ function isOrchestratorReferenced(ref, { categoryDir, stem }) {
593
+ // CLAUDE.md mentions any file in the category directory whose
594
+ // basename (minus leading number + dot) matches the sub-skill
595
+ // parent stem. This accepts `01.scaffold-crud-feature.md`,
596
+ // `scaffold-crud-feature.md`, etc.
597
+ if (!ref.startsWith(categoryDir)) return false;
598
+ const tail = ref.slice(categoryDir.length);
599
+ // Must be a sibling file, not a nested path.
600
+ if (tail.includes("/")) return false;
601
+ // Strip leading "NN." if present, then compare stem.
602
+ const base = tail.replace(/^\d+\./, "").replace(/\.md$/, "");
603
+ return base === stem;
604
+ }
605
+
606
+ for (const p of registered) {
607
+ if (referenced.has(p)) continue; // direct mention → OK
608
+
609
+ // Sub-skill exception.
610
+ const oc = orchestratorFor(p);
611
+ if (oc) {
612
+ const orchestratorMentioned = Array.from(referenced).some((ref) =>
613
+ isOrchestratorReferenced(ref, oc)
614
+ );
615
+ if (orchestratorMentioned) continue; // covered via orchestrator
616
+ }
617
+
618
+ manifestErrors++;
619
+ errors.push({
620
+ file: "CLAUDE.md",
621
+ type: "MANIFEST_DRIFT",
622
+ msg: `Skill "${p}" is registered in MANIFEST.md but not ` +
623
+ `mentioned in CLAUDE.md. Add it to CLAUDE.md §6 Skills ` +
624
+ `sub-section, or remove the row from MANIFEST if no ` +
625
+ `longer active.`,
626
+ });
627
+ }
628
+ }
629
+ console.log(` ${registered.size} skill(s) in MANIFEST, ${manifestErrors} drift issue(s)`);
630
+ } else {
631
+ // MANIFEST absence is not automatically an error — small projects
632
+ // may not use the skills system. Just note it.
633
+ console.log(" (no MANIFEST.md found — skipping)");
634
+ }
635
+
636
+ // ─── Output results ─────────────────────────────────────────
637
+ console.log(`\n Checked ${checked} files\n`);
638
+ if (errors.length) {
639
+ console.log(` ❌ ERRORS (${errors.length}):`);
640
+ errors.forEach(e => console.log(` [${e.type}] ${e.file}: ${e.msg}`));
641
+ console.log();
642
+ }
643
+ if (warnings.length) {
644
+ console.log(` ⚠️ WARNINGS (${warnings.length}):`);
645
+ warnings.forEach(w => console.log(` [${w.type}] ${w.file}: ${w.msg}`));
646
+ console.log();
647
+ }
648
+ if (!errors.length && !warnings.length) {
649
+ console.log(" ✅ All content validation passed\n");
650
+ }
651
+
652
+ // Record in stale-report
653
+ updateStaleReport(GEN_DIR, "contentValidation",
654
+ { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length, details: { errors, warnings } },
655
+ { contentErrors: errors.length, contentWarnings: warnings.length }
656
+ );
657
+
658
+ console.log(` Total: ${errors.length} errors, ${warnings.length} warnings\n`);
659
+ process.exit(errors.length > 0 ? 1 : 0);
660
+ }
661
+
662
+ if (require.main === module) {
663
+ main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
664
+ }
665
+
666
+ module.exports = { main };