claudeos-core 1.7.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/CONTRIBUTING.md +92 -59
- package/README.de.md +465 -240
- package/README.es.md +446 -223
- package/README.fr.md +461 -238
- package/README.hi.md +485 -261
- package/README.ja.md +440 -235
- package/README.ko.md +244 -56
- package/README.md +215 -47
- package/README.ru.md +462 -238
- package/README.vi.md +454 -230
- package/README.zh-CN.md +476 -252
- package/bin/cli.js +144 -140
- package/bin/commands/init.js +550 -46
- package/bin/commands/memory.js +426 -0
- package/bin/lib/cli-utils.js +206 -143
- package/bootstrap.sh +81 -390
- package/content-validator/index.js +436 -340
- package/lib/expected-guides.js +23 -0
- package/lib/expected-outputs.js +91 -0
- package/lib/language-config.js +35 -0
- package/lib/memory-scaffold.js +1014 -0
- package/lib/plan-parser.js +153 -149
- package/lib/staged-rules.js +118 -0
- package/manifest-generator/index.js +176 -171
- package/package.json +1 -1
- package/pass-json-validator/index.js +337 -299
- package/pass-prompts/templates/common/pass3-footer.md +16 -0
- package/pass-prompts/templates/common/pass4.md +317 -0
- package/pass-prompts/templates/common/staging-override.md +26 -0
- package/pass-prompts/templates/python-flask/pass1.md +119 -0
- package/pass-prompts/templates/python-flask/pass2.md +85 -0
- package/pass-prompts/templates/python-flask/pass3.md +103 -0
- package/plan-installer/domain-grouper.js +2 -1
- package/plan-installer/prompt-generator.js +120 -96
- package/plan-installer/scanners/scan-frontend.js +219 -10
- package/plan-installer/scanners/scan-java.js +226 -223
- package/plan-installer/scanners/scan-python.js +21 -0
- package/sync-checker/index.js +133 -132
|
@@ -1,299 +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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
// ───
|
|
270
|
-
console.log(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
console.log("
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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 };
|