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.
- package/CHANGELOG.md +1664 -907
- package/CONTRIBUTING.md +92 -92
- package/README.de.md +28 -0
- package/README.es.md +28 -0
- package/README.fr.md +28 -0
- package/README.hi.md +28 -0
- package/README.ja.md +28 -0
- package/README.ko.md +1014 -986
- package/README.md +1016 -987
- package/README.ru.md +28 -0
- package/README.vi.md +1015 -987
- package/README.zh-CN.md +28 -0
- package/bin/cli.js +152 -148
- package/bin/commands/init.js +1673 -1554
- package/bin/commands/lint.js +62 -0
- package/bin/commands/memory.js +438 -438
- package/bin/lib/cli-utils.js +206 -206
- package/claude-md-validator/index.js +184 -0
- package/claude-md-validator/reporter.js +66 -0
- package/claude-md-validator/structural-checks.js +528 -0
- package/content-validator/index.js +666 -441
- package/lib/expected-guides.js +23 -23
- package/lib/expected-outputs.js +90 -90
- package/lib/language-config.js +35 -35
- package/lib/memory-scaffold.js +1058 -1054
- package/lib/plan-parser.js +165 -165
- package/lib/staged-rules.js +118 -118
- package/manifest-generator/index.js +174 -174
- package/package.json +90 -87
- package/pass-json-validator/index.js +337 -337
- package/pass-prompts/templates/common/claude-md-scaffold.md +52 -10
- package/pass-prompts/templates/common/pass3-footer.md +402 -224
- package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
- package/pass-prompts/templates/common/pass4.md +375 -305
- package/pass-prompts/templates/common/staging-override.md +26 -26
- package/pass-prompts/templates/node-vite/pass1.md +117 -117
- package/pass-prompts/templates/node-vite/pass2.md +78 -78
- package/pass-prompts/templates/python-flask/pass1.md +119 -119
- package/pass-prompts/templates/python-flask/pass2.md +85 -85
- package/plan-installer/domain-grouper.js +76 -76
- package/plan-installer/index.js +137 -137
- package/plan-installer/prompt-generator.js +188 -145
- package/plan-installer/scanners/scan-frontend.js +505 -473
- package/plan-installer/scanners/scan-java.js +226 -226
- package/plan-installer/scanners/scan-node.js +57 -57
- package/plan-installer/scanners/scan-python.js +85 -85
- package/plan-installer/stack-detector.js +482 -482
- package/plan-installer/structure-scanner.js +65 -65
- 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
|
-
// ───
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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 };
|