claudeos-core 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +1649 -481
  2. package/CONTRIBUTING.md +92 -92
  3. package/README.de.md +64 -5
  4. package/README.es.md +64 -5
  5. package/README.fr.md +64 -5
  6. package/README.hi.md +64 -5
  7. package/README.ja.md +64 -5
  8. package/README.ko.md +1018 -959
  9. package/README.md +1020 -960
  10. package/README.ru.md +66 -5
  11. package/README.vi.md +1019 -960
  12. package/README.zh-CN.md +64 -5
  13. package/bin/cli.js +152 -148
  14. package/bin/commands/init.js +1673 -1518
  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 -436
  22. package/lib/env-parser.js +317 -0
  23. package/lib/expected-guides.js +23 -23
  24. package/lib/expected-outputs.js +90 -90
  25. package/lib/language-config.js +35 -35
  26. package/lib/memory-scaffold.js +1058 -1052
  27. package/lib/plan-parser.js +165 -165
  28. package/lib/staged-rules.js +118 -118
  29. package/manifest-generator/index.js +174 -174
  30. package/package.json +90 -87
  31. package/pass-json-validator/index.js +337 -337
  32. package/pass-prompts/templates/angular/pass3.md +28 -13
  33. package/pass-prompts/templates/common/claude-md-scaffold.md +686 -0
  34. package/pass-prompts/templates/common/pass3-footer.md +402 -39
  35. package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
  36. package/pass-prompts/templates/common/pass4.md +375 -302
  37. package/pass-prompts/templates/common/staging-override.md +26 -26
  38. package/pass-prompts/templates/java-spring/pass3.md +31 -21
  39. package/pass-prompts/templates/kotlin-spring/pass3.md +34 -22
  40. package/pass-prompts/templates/node-express/pass3.md +30 -21
  41. package/pass-prompts/templates/node-fastify/pass3.md +28 -14
  42. package/pass-prompts/templates/node-nestjs/pass3.md +29 -14
  43. package/pass-prompts/templates/node-nextjs/pass3.md +34 -21
  44. package/pass-prompts/templates/node-vite/pass1.md +117 -117
  45. package/pass-prompts/templates/node-vite/pass2.md +78 -78
  46. package/pass-prompts/templates/node-vite/pass3.md +30 -13
  47. package/pass-prompts/templates/python-django/pass3.md +32 -21
  48. package/pass-prompts/templates/python-fastapi/pass3.md +33 -21
  49. package/pass-prompts/templates/python-flask/pass1.md +119 -119
  50. package/pass-prompts/templates/python-flask/pass2.md +85 -85
  51. package/pass-prompts/templates/python-flask/pass3.md +31 -13
  52. package/pass-prompts/templates/vue-nuxt/pass3.md +32 -13
  53. package/plan-installer/domain-grouper.js +76 -76
  54. package/plan-installer/index.js +137 -129
  55. package/plan-installer/prompt-generator.js +188 -128
  56. package/plan-installer/scanners/scan-frontend.js +505 -473
  57. package/plan-installer/scanners/scan-java.js +226 -226
  58. package/plan-installer/scanners/scan-node.js +57 -57
  59. package/plan-installer/scanners/scan-python.js +85 -85
  60. package/plan-installer/stack-detector.js +482 -466
  61. package/plan-installer/structure-scanner.js +65 -65
  62. package/sync-checker/index.js +177 -177
@@ -1,337 +1,337 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * ClaudeOS-Core — Pass JSON Validator
5
- *
6
- * Role: Validate format and required keys of JSON files generated by Pass 1-3
7
- * Validation items:
8
- * - JSON is parseable
9
- * - pass1-*.json: analyzedAt, passNum, domains, analysisPerDomain keys exist
10
- * - pass2-merged.json: fuzzy validation of common required sections (10) + backend sections (2)
11
- * empty value detection, top-level key count check (<5 ERROR, <9 WARNING)
12
- * - project-analysis.json: stack, domains, frontend, summary keys exist
13
- *
14
- * Usage: npx claudeos-core <cmd> or node claudeos-core-tools/pass-json-validator/index.js
15
- */
16
-
17
- const fs = require("fs");
18
- const path = require("path");
19
- const { glob } = require("glob");
20
- const { updateStaleReport } = require("../lib/stale-report");
21
-
22
- const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
23
- const GEN_DIR = path.join(ROOT, "claudeos-core/generated");
24
-
25
- function rel(p) { return path.relative(ROOT, p).replace(/\\/g, "/"); }
26
-
27
- async function main() {
28
- console.log("\n╔═══════════════════════════════════════╗");
29
- console.log("║ ClaudeOS-Core — Pass JSON Validator ║");
30
- console.log("╚═══════════════════════════════════════╝\n");
31
-
32
- if (!fs.existsSync(GEN_DIR)) {
33
- console.log(" ❌ claudeos-core/generated/ directory not found\n");
34
- process.exit(1);
35
- }
36
-
37
- const errors = [];
38
- const warnings = [];
39
- let checked = 0;
40
-
41
- // ─── Helpers: JSON parse + key validation ────────────────────────
42
- function validateJson(filePath, requiredKeys, optionalKeys) {
43
- const r = rel(filePath);
44
- if (!fs.existsSync(filePath)) {
45
- errors.push({ file: r, type: "MISSING", msg: "File not found" });
46
- return null;
47
- }
48
- checked++;
49
- let data;
50
- try {
51
- const raw = fs.readFileSync(filePath, "utf-8");
52
- data = JSON.parse(raw);
53
- } catch (e) {
54
- errors.push({ file: r, type: "PARSE_ERROR", msg: `JSON parse failed: ${e.message}` });
55
- return null;
56
- }
57
-
58
- for (const key of requiredKeys) {
59
- if (!(key in data)) {
60
- errors.push({ file: r, type: "MISSING_KEY", msg: `Required key '${key}' missing` });
61
- }
62
- }
63
- for (const key of (optionalKeys || [])) {
64
- if (!(key in data)) {
65
- warnings.push({ file: r, type: "MISSING_KEY", msg: `Recommended key '${key}' missing` });
66
- }
67
- }
68
- return data;
69
- }
70
-
71
- // ─── 1. project-analysis.json ──────────────────────────
72
- console.log(" [1/5] project-analysis.json...");
73
- const pa = validateJson(
74
- path.join(GEN_DIR, "project-analysis.json"),
75
- ["analyzedAt", "stack", "domains", "frontend", "summary"],
76
- ["lang", "template", "templates", "rootPackage", "activeDomains"]
77
- );
78
- if (pa) {
79
- if (!pa.stack || !pa.stack.language) {
80
- errors.push({ file: "project-analysis.json", type: "INVALID_STACK", msg: "stack.language is missing" });
81
- }
82
- if (!Array.isArray(pa.domains)) {
83
- errors.push({ file: "project-analysis.json", type: "INVALID_DOMAINS", msg: "domains is not an array" });
84
- } else {
85
- console.log(` stack: ${pa.stack?.language || "?"} / ${pa.stack?.framework || "?"}`);
86
- console.log(` domains: ${pa.domains.length}`);
87
- console.log(` lang: ${pa.lang || "en (default)"}`);
88
- const tmpl = pa.templates || pa.template;
89
- if (tmpl && typeof tmpl === "object") {
90
- console.log(` templates: backend=${tmpl.backend || "none"}, frontend=${tmpl.frontend || "none"}`);
91
- if (pa.isMultiStack) console.log(` mode: 🔀 multi-stack`);
92
- } else {
93
- console.log(` template: ${tmpl || "?"}`);
94
- }
95
- }
96
- }
97
-
98
- // ─── 2. domain-groups.json ─────────────────────────────
99
- console.log(" [2/5] domain-groups.json...");
100
- const dg = validateJson(
101
- path.join(GEN_DIR, "domain-groups.json"),
102
- ["generatedAt", "totalDomains", "totalGroups", "groups"],
103
- []
104
- );
105
- if (dg) {
106
- if (!Array.isArray(dg.groups)) {
107
- errors.push({ file: "domain-groups.json", type: "INVALID_GROUPS", msg: "groups is not an array" });
108
- } else {
109
- console.log(` ${dg.totalGroups} groups, ${dg.totalDomains} domains`);
110
- }
111
- }
112
-
113
- // ─── 3. pass1-*.json ──────────────────────────────────
114
- console.log(" [3/5] pass1-*.json...");
115
- const pass1JsonFiles = await glob("pass1-*.json", { cwd: GEN_DIR, absolute: true });
116
- if (pass1JsonFiles.length === 0) {
117
- warnings.push({ file: "pass1-*.json", type: "NO_FILES", msg: "No pass1 JSON found (not yet executed?)" });
118
- }
119
- for (const f of pass1JsonFiles) {
120
- const data = validateJson(f,
121
- ["analyzedAt", "passNum", "domains", "analysisPerDomain"],
122
- ["crossDomainCommon"]
123
- );
124
- if (data) {
125
- if (!Array.isArray(data.domains) || data.domains.length === 0) {
126
- warnings.push({ file: rel(f), type: "EMPTY_DOMAINS", msg: "No analyzed domains found" });
127
- }
128
- if (typeof data.analysisPerDomain !== "object") {
129
- errors.push({ file: rel(f), type: "INVALID_ANALYSIS", msg: "analysisPerDomain is not an object" });
130
- } else {
131
- const domainCount = Object.keys(data.analysisPerDomain).length;
132
- console.log(` ${path.basename(f)}: ${domainCount} domains analyzed`);
133
- }
134
- }
135
- }
136
-
137
- // ─── 4. pass2-merged.json ─────────────────────────────
138
- console.log(" [4/5] pass2-merged.json...");
139
- const p2path = path.join(GEN_DIR, "pass2-merged.json");
140
- if (!fs.existsSync(p2path)) {
141
- warnings.push({ file: "pass2-merged.json", type: "NO_FILE", msg: "Not yet generated (Pass 2 not executed?)" });
142
- } else {
143
- checked++;
144
- let data;
145
- try {
146
- data = JSON.parse(fs.readFileSync(p2path, "utf-8"));
147
- } catch (e) {
148
- errors.push({ file: "pass2-merged.json", type: "PARSE_ERROR", msg: `JSON parse failed: ${e.message}` });
149
- data = null;
150
- }
151
-
152
- if (data) {
153
- const keys = Object.keys(data);
154
- console.log(` ${keys.length} top-level keys: ${keys.slice(0, 5).join(", ")}${keys.length > 5 ? " ..." : ""}`);
155
-
156
- // Required sections common to all stacks (intersection of all 5 stack pass2 prompts)
157
- const REQUIRED_SECTIONS = [
158
- "commonPatterns", // 1. Universal patterns
159
- "sharedPatterns", // 2. Majority patterns
160
- "domainSpecific", // 3. Domain-specific patterns
161
- "antiPatterns", // 4. Anti-pattern summary
162
- "namingConventions", // 5. Naming conventions summary
163
- "commonUtilities", // 6. Common classes/utilities list
164
- "security", // 7. Security/auth patterns
165
- "testing", // 9. Testing strategy summary
166
- "logging", // 10. Logging/monitoring strategy
167
- "codeQuality", // 12. Code quality tools
168
- ];
169
-
170
- // Sections only in backend stacks (java/node-express/django/fastapi/kotlin)
171
- const BACKEND_SECTIONS = [
172
- "database", // 8. DB patterns
173
- "performance", // 11. Performance patterns
174
- ];
175
-
176
- // Sections only in kotlin-spring with CQRS/BFF/multi-module detected
177
- const KOTLIN_CQRS_SECTIONS = [
178
- "bffPatterns", // 7. BFF patterns summary
179
- "interModuleCommunication", // Inter-module communication
180
- ];
181
- // Sections for all kotlin-spring projects (regardless of CQRS)
182
- const KOTLIN_BASE_SECTIONS = [
183
- "buildModulePatterns", // 13. Build & module patterns
184
- ];
185
-
186
- // Check stack from project-analysis.json
187
- const paPath = path.join(GEN_DIR, "project-analysis.json");
188
- let isBackend = true; // Default: also validate backend sections
189
- let isKotlin = false;
190
- let isKotlinCqrs = false;
191
- if (fs.existsSync(paPath)) {
192
- try {
193
- const paData = JSON.parse(fs.readFileSync(paPath, "utf-8"));
194
- const frontend = paData.stack?.frontend;
195
- const framework = paData.stack?.framework;
196
- const language = paData.stack?.language;
197
- const architecture = paData.stack?.architecture;
198
- isBackend = !frontend || ["express", "nestjs", "fastify", "django", "fastapi", "flask", "spring-boot"].includes(framework);
199
- isKotlin = language === "kotlin";
200
- isKotlinCqrs = isKotlin && (architecture === "cqrs" || paData.stack?.multiModule);
201
- } catch (_e) { /* If project-analysis parsing fails, conservatively assume backend */ }
202
- }
203
-
204
- const sectionsToCheck = [
205
- ...REQUIRED_SECTIONS,
206
- ...(isBackend ? BACKEND_SECTIONS : []),
207
- ...(isKotlin ? KOTLIN_BASE_SECTIONS : []),
208
- ...(isKotlinCqrs ? KOTLIN_CQRS_SECTIONS : []),
209
- ];
210
-
211
- // Key existence validation (case-insensitive fuzzy matching)
212
- // Normalize a key to a comparable form: lowercase, strip separators
213
- const normalize = (s) => s.toLowerCase().replace(/[_\-\s]/g, "");
214
- const normalizedKeys = keys.map(normalize);
215
- for (const section of sectionsToCheck) {
216
- const sectionNorm = normalize(section);
217
- // Exact normalized match first, then check if any key contains the section name (one-direction only to avoid false positives)
218
- const found = normalizedKeys.some(k => k === sectionNorm)
219
- || (sectionNorm.length >= 6 && normalizedKeys.some(k => k.includes(sectionNorm)));
220
- if (!found) {
221
- warnings.push({
222
- file: "pass2-merged.json",
223
- type: "MISSING_SECTION",
224
- msg: `'${section}' section missing (Claude may have used a different key name)`,
225
- });
226
- }
227
- }
228
-
229
- // Top-level key count validation
230
- if (keys.length < 5) {
231
- errors.push({
232
- file: "pass2-merged.json",
233
- type: "INSUFFICIENT_KEYS",
234
- msg: `Only ${keys.length} top-level keys — merge is incomplete (minimum 5 required)`,
235
- });
236
- } else if (keys.length < 9) {
237
- warnings.push({
238
- file: "pass2-merged.json",
239
- type: "FEW_KEYS",
240
- msg: `${keys.length} top-level keys — some sections may be missing (recommended 9+)`,
241
- });
242
- }
243
-
244
- // Value depth validation — detect empty objects/arrays
245
- let emptyCount = 0;
246
- for (const [k, v] of Object.entries(data)) {
247
- const isEmpty =
248
- v === null ||
249
- v === "" ||
250
- (Array.isArray(v) && v.length === 0) ||
251
- (typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0);
252
- if (isEmpty) emptyCount++;
253
- }
254
- if (emptyCount > 0) {
255
- warnings.push({
256
- file: "pass2-merged.json",
257
- type: "EMPTY_VALUES",
258
- msg: `${emptyCount} items have empty values — analysis content may be missing`,
259
- });
260
- }
261
-
262
- const missingCount = warnings.filter(w => w.file === "pass2-merged.json" && w.type === "MISSING_SECTION").length;
263
- if (missingCount === 0 && keys.length >= 9) {
264
- console.log(` ✅ Structure validation passed (${keys.length} keys, ${emptyCount} empty values)`);
265
- }
266
- }
267
- }
268
-
269
- // ─── 5a. pass3-complete.json (optional) ──────────────────
270
- console.log(" [5a/5] pass3-complete.json (optional)...");
271
- const p3path = path.join(GEN_DIR, "pass3-complete.json");
272
- if (fs.existsSync(p3path)) {
273
- const p3 = validateJson(p3path, ["completedAt"], ["backfilled", "reason"]);
274
- if (p3) {
275
- if (typeof p3.completedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(p3.completedAt)) {
276
- warnings.push({ file: "pass3-complete.json", type: "INVALID_TIMESTAMP", msg: `completedAt should be ISO 8601 (got ${JSON.stringify(p3.completedAt)})` });
277
- } else {
278
- console.log(` ✅ completedAt=${p3.completedAt}${p3.backfilled ? " (backfilled)" : ""}`);
279
- }
280
- }
281
- } else {
282
- console.log(" ⏭️ not present (Pass 3 not yet run)");
283
- }
284
-
285
- // ─── 5b. pass4-memory.json (optional) ─────────────
286
- console.log(" [5b/5] pass4-memory.json (optional)...");
287
- const p4path = path.join(GEN_DIR, "pass4-memory.json");
288
- if (fs.existsSync(p4path)) {
289
- const p4 = validateJson(p4path,
290
- ["analyzedAt", "passNum", "memoryFiles"],
291
- ["planFiles", "ruleFiles", "seededDecisions", "fallback"]
292
- );
293
- if (p4) {
294
- if (typeof p4.passNum !== "number" || p4.passNum !== 4) {
295
- errors.push({ file: "pass4-memory.json", type: "INVALID_PASS_NUM", msg: `passNum must be number 4 (got ${JSON.stringify(p4.passNum)})` });
296
- }
297
- if (!Array.isArray(p4.memoryFiles) || p4.memoryFiles.length !== 4) {
298
- errors.push({ file: "pass4-memory.json", type: "INVALID_MEMORY", msg: `memoryFiles must be an array of 4 paths (got ${Array.isArray(p4.memoryFiles) ? p4.memoryFiles.length : typeof p4.memoryFiles})` });
299
- } else {
300
- console.log(` ✅ passNum=${p4.passNum}, memory=${p4.memoryFiles.length}${p4.fallback ? " (fallback)" : ""}`);
301
- }
302
- }
303
- } else {
304
- warnings.push({ file: "pass4-memory.json", type: "NO_FILE", msg: "Not yet generated (Pass 4 not executed?)" });
305
- }
306
-
307
- // ─── Output results ─────────────────────────────────────────
308
- console.log(`\n Checked ${checked} files\n`);
309
- if (errors.length) {
310
- console.log(` ❌ ERRORS (${errors.length}):`);
311
- errors.forEach(e => console.log(` [${e.type}] ${e.file}: ${e.msg}`));
312
- console.log();
313
- }
314
- if (warnings.length) {
315
- console.log(` ⚠️ WARNINGS (${warnings.length}):`);
316
- warnings.forEach(w => console.log(` [${w.type}] ${w.file}: ${w.msg}`));
317
- console.log();
318
- }
319
- if (!errors.length && !warnings.length) {
320
- console.log(" ✅ All JSON validation passed\n");
321
- }
322
-
323
- // Record in stale-report
324
- updateStaleReport(GEN_DIR, "jsonValidation",
325
- { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length },
326
- { jsonErrors: errors.length, jsonWarnings: warnings.length }
327
- );
328
-
329
- console.log(` Total: ${errors.length} errors, ${warnings.length} warnings\n`);
330
- process.exit(errors.length > 0 ? 1 : 0);
331
- }
332
-
333
- if (require.main === module) {
334
- main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
335
- }
336
-
337
- module.exports = { main };
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ClaudeOS-Core — Pass JSON Validator
5
+ *
6
+ * Role: Validate format and required keys of JSON files generated by Pass 1-3
7
+ * Validation items:
8
+ * - JSON is parseable
9
+ * - pass1-*.json: analyzedAt, passNum, domains, analysisPerDomain keys exist
10
+ * - pass2-merged.json: fuzzy validation of common required sections (10) + backend sections (2)
11
+ * empty value detection, top-level key count check (<5 ERROR, <9 WARNING)
12
+ * - project-analysis.json: stack, domains, frontend, summary keys exist
13
+ *
14
+ * Usage: npx claudeos-core <cmd> or node claudeos-core-tools/pass-json-validator/index.js
15
+ */
16
+
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+ const { glob } = require("glob");
20
+ const { updateStaleReport } = require("../lib/stale-report");
21
+
22
+ const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
23
+ const GEN_DIR = path.join(ROOT, "claudeos-core/generated");
24
+
25
+ function rel(p) { return path.relative(ROOT, p).replace(/\\/g, "/"); }
26
+
27
+ async function main() {
28
+ console.log("\n╔═══════════════════════════════════════╗");
29
+ console.log("║ ClaudeOS-Core — Pass JSON Validator ║");
30
+ console.log("╚═══════════════════════════════════════╝\n");
31
+
32
+ if (!fs.existsSync(GEN_DIR)) {
33
+ console.log(" ❌ claudeos-core/generated/ directory not found\n");
34
+ process.exit(1);
35
+ }
36
+
37
+ const errors = [];
38
+ const warnings = [];
39
+ let checked = 0;
40
+
41
+ // ─── Helpers: JSON parse + key validation ────────────────────────
42
+ function validateJson(filePath, requiredKeys, optionalKeys) {
43
+ const r = rel(filePath);
44
+ if (!fs.existsSync(filePath)) {
45
+ errors.push({ file: r, type: "MISSING", msg: "File not found" });
46
+ return null;
47
+ }
48
+ checked++;
49
+ let data;
50
+ try {
51
+ const raw = fs.readFileSync(filePath, "utf-8");
52
+ data = JSON.parse(raw);
53
+ } catch (e) {
54
+ errors.push({ file: r, type: "PARSE_ERROR", msg: `JSON parse failed: ${e.message}` });
55
+ return null;
56
+ }
57
+
58
+ for (const key of requiredKeys) {
59
+ if (!(key in data)) {
60
+ errors.push({ file: r, type: "MISSING_KEY", msg: `Required key '${key}' missing` });
61
+ }
62
+ }
63
+ for (const key of (optionalKeys || [])) {
64
+ if (!(key in data)) {
65
+ warnings.push({ file: r, type: "MISSING_KEY", msg: `Recommended key '${key}' missing` });
66
+ }
67
+ }
68
+ return data;
69
+ }
70
+
71
+ // ─── 1. project-analysis.json ──────────────────────────
72
+ console.log(" [1/5] project-analysis.json...");
73
+ const pa = validateJson(
74
+ path.join(GEN_DIR, "project-analysis.json"),
75
+ ["analyzedAt", "stack", "domains", "frontend", "summary"],
76
+ ["lang", "template", "templates", "rootPackage", "activeDomains"]
77
+ );
78
+ if (pa) {
79
+ if (!pa.stack || !pa.stack.language) {
80
+ errors.push({ file: "project-analysis.json", type: "INVALID_STACK", msg: "stack.language is missing" });
81
+ }
82
+ if (!Array.isArray(pa.domains)) {
83
+ errors.push({ file: "project-analysis.json", type: "INVALID_DOMAINS", msg: "domains is not an array" });
84
+ } else {
85
+ console.log(` stack: ${pa.stack?.language || "?"} / ${pa.stack?.framework || "?"}`);
86
+ console.log(` domains: ${pa.domains.length}`);
87
+ console.log(` lang: ${pa.lang || "en (default)"}`);
88
+ const tmpl = pa.templates || pa.template;
89
+ if (tmpl && typeof tmpl === "object") {
90
+ console.log(` templates: backend=${tmpl.backend || "none"}, frontend=${tmpl.frontend || "none"}`);
91
+ if (pa.isMultiStack) console.log(` mode: 🔀 multi-stack`);
92
+ } else {
93
+ console.log(` template: ${tmpl || "?"}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ // ─── 2. domain-groups.json ─────────────────────────────
99
+ console.log(" [2/5] domain-groups.json...");
100
+ const dg = validateJson(
101
+ path.join(GEN_DIR, "domain-groups.json"),
102
+ ["generatedAt", "totalDomains", "totalGroups", "groups"],
103
+ []
104
+ );
105
+ if (dg) {
106
+ if (!Array.isArray(dg.groups)) {
107
+ errors.push({ file: "domain-groups.json", type: "INVALID_GROUPS", msg: "groups is not an array" });
108
+ } else {
109
+ console.log(` ${dg.totalGroups} groups, ${dg.totalDomains} domains`);
110
+ }
111
+ }
112
+
113
+ // ─── 3. pass1-*.json ──────────────────────────────────
114
+ console.log(" [3/5] pass1-*.json...");
115
+ const pass1JsonFiles = await glob("pass1-*.json", { cwd: GEN_DIR, absolute: true });
116
+ if (pass1JsonFiles.length === 0) {
117
+ warnings.push({ file: "pass1-*.json", type: "NO_FILES", msg: "No pass1 JSON found (not yet executed?)" });
118
+ }
119
+ for (const f of pass1JsonFiles) {
120
+ const data = validateJson(f,
121
+ ["analyzedAt", "passNum", "domains", "analysisPerDomain"],
122
+ ["crossDomainCommon"]
123
+ );
124
+ if (data) {
125
+ if (!Array.isArray(data.domains) || data.domains.length === 0) {
126
+ warnings.push({ file: rel(f), type: "EMPTY_DOMAINS", msg: "No analyzed domains found" });
127
+ }
128
+ if (typeof data.analysisPerDomain !== "object") {
129
+ errors.push({ file: rel(f), type: "INVALID_ANALYSIS", msg: "analysisPerDomain is not an object" });
130
+ } else {
131
+ const domainCount = Object.keys(data.analysisPerDomain).length;
132
+ console.log(` ${path.basename(f)}: ${domainCount} domains analyzed`);
133
+ }
134
+ }
135
+ }
136
+
137
+ // ─── 4. pass2-merged.json ─────────────────────────────
138
+ console.log(" [4/5] pass2-merged.json...");
139
+ const p2path = path.join(GEN_DIR, "pass2-merged.json");
140
+ if (!fs.existsSync(p2path)) {
141
+ warnings.push({ file: "pass2-merged.json", type: "NO_FILE", msg: "Not yet generated (Pass 2 not executed?)" });
142
+ } else {
143
+ checked++;
144
+ let data;
145
+ try {
146
+ data = JSON.parse(fs.readFileSync(p2path, "utf-8"));
147
+ } catch (e) {
148
+ errors.push({ file: "pass2-merged.json", type: "PARSE_ERROR", msg: `JSON parse failed: ${e.message}` });
149
+ data = null;
150
+ }
151
+
152
+ if (data) {
153
+ const keys = Object.keys(data);
154
+ console.log(` ${keys.length} top-level keys: ${keys.slice(0, 5).join(", ")}${keys.length > 5 ? " ..." : ""}`);
155
+
156
+ // Required sections common to all stacks (intersection of all 5 stack pass2 prompts)
157
+ const REQUIRED_SECTIONS = [
158
+ "commonPatterns", // 1. Universal patterns
159
+ "sharedPatterns", // 2. Majority patterns
160
+ "domainSpecific", // 3. Domain-specific patterns
161
+ "antiPatterns", // 4. Anti-pattern summary
162
+ "namingConventions", // 5. Naming conventions summary
163
+ "commonUtilities", // 6. Common classes/utilities list
164
+ "security", // 7. Security/auth patterns
165
+ "testing", // 9. Testing strategy summary
166
+ "logging", // 10. Logging/monitoring strategy
167
+ "codeQuality", // 12. Code quality tools
168
+ ];
169
+
170
+ // Sections only in backend stacks (java/node-express/django/fastapi/kotlin)
171
+ const BACKEND_SECTIONS = [
172
+ "database", // 8. DB patterns
173
+ "performance", // 11. Performance patterns
174
+ ];
175
+
176
+ // Sections only in kotlin-spring with CQRS/BFF/multi-module detected
177
+ const KOTLIN_CQRS_SECTIONS = [
178
+ "bffPatterns", // 7. BFF patterns summary
179
+ "interModuleCommunication", // Inter-module communication
180
+ ];
181
+ // Sections for all kotlin-spring projects (regardless of CQRS)
182
+ const KOTLIN_BASE_SECTIONS = [
183
+ "buildModulePatterns", // 13. Build & module patterns
184
+ ];
185
+
186
+ // Check stack from project-analysis.json
187
+ const paPath = path.join(GEN_DIR, "project-analysis.json");
188
+ let isBackend = true; // Default: also validate backend sections
189
+ let isKotlin = false;
190
+ let isKotlinCqrs = false;
191
+ if (fs.existsSync(paPath)) {
192
+ try {
193
+ const paData = JSON.parse(fs.readFileSync(paPath, "utf-8"));
194
+ const frontend = paData.stack?.frontend;
195
+ const framework = paData.stack?.framework;
196
+ const language = paData.stack?.language;
197
+ const architecture = paData.stack?.architecture;
198
+ isBackend = !frontend || ["express", "nestjs", "fastify", "django", "fastapi", "flask", "spring-boot"].includes(framework);
199
+ isKotlin = language === "kotlin";
200
+ isKotlinCqrs = isKotlin && (architecture === "cqrs" || paData.stack?.multiModule);
201
+ } catch (_e) { /* If project-analysis parsing fails, conservatively assume backend */ }
202
+ }
203
+
204
+ const sectionsToCheck = [
205
+ ...REQUIRED_SECTIONS,
206
+ ...(isBackend ? BACKEND_SECTIONS : []),
207
+ ...(isKotlin ? KOTLIN_BASE_SECTIONS : []),
208
+ ...(isKotlinCqrs ? KOTLIN_CQRS_SECTIONS : []),
209
+ ];
210
+
211
+ // Key existence validation (case-insensitive fuzzy matching)
212
+ // Normalize a key to a comparable form: lowercase, strip separators
213
+ const normalize = (s) => s.toLowerCase().replace(/[_\-\s]/g, "");
214
+ const normalizedKeys = keys.map(normalize);
215
+ for (const section of sectionsToCheck) {
216
+ const sectionNorm = normalize(section);
217
+ // Exact normalized match first, then check if any key contains the section name (one-direction only to avoid false positives)
218
+ const found = normalizedKeys.some(k => k === sectionNorm)
219
+ || (sectionNorm.length >= 6 && normalizedKeys.some(k => k.includes(sectionNorm)));
220
+ if (!found) {
221
+ warnings.push({
222
+ file: "pass2-merged.json",
223
+ type: "MISSING_SECTION",
224
+ msg: `'${section}' section missing (Claude may have used a different key name)`,
225
+ });
226
+ }
227
+ }
228
+
229
+ // Top-level key count validation
230
+ if (keys.length < 5) {
231
+ errors.push({
232
+ file: "pass2-merged.json",
233
+ type: "INSUFFICIENT_KEYS",
234
+ msg: `Only ${keys.length} top-level keys — merge is incomplete (minimum 5 required)`,
235
+ });
236
+ } else if (keys.length < 9) {
237
+ warnings.push({
238
+ file: "pass2-merged.json",
239
+ type: "FEW_KEYS",
240
+ msg: `${keys.length} top-level keys — some sections may be missing (recommended 9+)`,
241
+ });
242
+ }
243
+
244
+ // Value depth validation — detect empty objects/arrays
245
+ let emptyCount = 0;
246
+ for (const [k, v] of Object.entries(data)) {
247
+ const isEmpty =
248
+ v === null ||
249
+ v === "" ||
250
+ (Array.isArray(v) && v.length === 0) ||
251
+ (typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0);
252
+ if (isEmpty) emptyCount++;
253
+ }
254
+ if (emptyCount > 0) {
255
+ warnings.push({
256
+ file: "pass2-merged.json",
257
+ type: "EMPTY_VALUES",
258
+ msg: `${emptyCount} items have empty values — analysis content may be missing`,
259
+ });
260
+ }
261
+
262
+ const missingCount = warnings.filter(w => w.file === "pass2-merged.json" && w.type === "MISSING_SECTION").length;
263
+ if (missingCount === 0 && keys.length >= 9) {
264
+ console.log(` ✅ Structure validation passed (${keys.length} keys, ${emptyCount} empty values)`);
265
+ }
266
+ }
267
+ }
268
+
269
+ // ─── 5a. pass3-complete.json (optional) ──────────────────
270
+ console.log(" [5a/5] pass3-complete.json (optional)...");
271
+ const p3path = path.join(GEN_DIR, "pass3-complete.json");
272
+ if (fs.existsSync(p3path)) {
273
+ const p3 = validateJson(p3path, ["completedAt"], ["backfilled", "reason"]);
274
+ if (p3) {
275
+ if (typeof p3.completedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T/.test(p3.completedAt)) {
276
+ warnings.push({ file: "pass3-complete.json", type: "INVALID_TIMESTAMP", msg: `completedAt should be ISO 8601 (got ${JSON.stringify(p3.completedAt)})` });
277
+ } else {
278
+ console.log(` ✅ completedAt=${p3.completedAt}${p3.backfilled ? " (backfilled)" : ""}`);
279
+ }
280
+ }
281
+ } else {
282
+ console.log(" ⏭️ not present (Pass 3 not yet run)");
283
+ }
284
+
285
+ // ─── 5b. pass4-memory.json (optional) ─────────────
286
+ console.log(" [5b/5] pass4-memory.json (optional)...");
287
+ const p4path = path.join(GEN_DIR, "pass4-memory.json");
288
+ if (fs.existsSync(p4path)) {
289
+ const p4 = validateJson(p4path,
290
+ ["analyzedAt", "passNum", "memoryFiles"],
291
+ ["planFiles", "ruleFiles", "seededDecisions", "fallback"]
292
+ );
293
+ if (p4) {
294
+ if (typeof p4.passNum !== "number" || p4.passNum !== 4) {
295
+ errors.push({ file: "pass4-memory.json", type: "INVALID_PASS_NUM", msg: `passNum must be number 4 (got ${JSON.stringify(p4.passNum)})` });
296
+ }
297
+ if (!Array.isArray(p4.memoryFiles) || p4.memoryFiles.length !== 4) {
298
+ errors.push({ file: "pass4-memory.json", type: "INVALID_MEMORY", msg: `memoryFiles must be an array of 4 paths (got ${Array.isArray(p4.memoryFiles) ? p4.memoryFiles.length : typeof p4.memoryFiles})` });
299
+ } else {
300
+ console.log(` ✅ passNum=${p4.passNum}, memory=${p4.memoryFiles.length}${p4.fallback ? " (fallback)" : ""}`);
301
+ }
302
+ }
303
+ } else {
304
+ warnings.push({ file: "pass4-memory.json", type: "NO_FILE", msg: "Not yet generated (Pass 4 not executed?)" });
305
+ }
306
+
307
+ // ─── Output results ─────────────────────────────────────────
308
+ console.log(`\n Checked ${checked} files\n`);
309
+ if (errors.length) {
310
+ console.log(` ❌ ERRORS (${errors.length}):`);
311
+ errors.forEach(e => console.log(` [${e.type}] ${e.file}: ${e.msg}`));
312
+ console.log();
313
+ }
314
+ if (warnings.length) {
315
+ console.log(` ⚠️ WARNINGS (${warnings.length}):`);
316
+ warnings.forEach(w => console.log(` [${w.type}] ${w.file}: ${w.msg}`));
317
+ console.log();
318
+ }
319
+ if (!errors.length && !warnings.length) {
320
+ console.log(" ✅ All JSON validation passed\n");
321
+ }
322
+
323
+ // Record in stale-report
324
+ updateStaleReport(GEN_DIR, "jsonValidation",
325
+ { checkedAt: new Date().toISOString(), checked, errors: errors.length, warnings: warnings.length },
326
+ { jsonErrors: errors.length, jsonWarnings: warnings.length }
327
+ );
328
+
329
+ console.log(` Total: ${errors.length} errors, ${warnings.length} warnings\n`);
330
+ process.exit(errors.length > 0 ? 1 : 0);
331
+ }
332
+
333
+ if (require.main === module) {
334
+ main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
335
+ }
336
+
337
+ module.exports = { main };