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,65 +1,65 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ClaudeOS-Core — Structure Scanner (Orchestrator)
|
|
3
|
-
*
|
|
4
|
-
* Scans project directory structure to discover domains (backend + frontend).
|
|
5
|
-
* Delegates to language-specific scanners in ./scanners/.
|
|
6
|
-
*
|
|
7
|
-
* Supported scanners:
|
|
8
|
-
* - scan-java.js — Java (5 patterns: A/B/C/D/E + fallback)
|
|
9
|
-
* - scan-kotlin.js — Kotlin (multi-module + CQRS + single fallback)
|
|
10
|
-
* - scan-node.js — Node.js (Express/NestJS/Fastify)
|
|
11
|
-
* - scan-python.js — Python (Django/FastAPI/Flask)
|
|
12
|
-
* - scan-frontend.js — Angular, Next.js, React, Vue (+ 4-stage fallback + stats)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const { scanJavaDomains } = require("./scanners/scan-java");
|
|
16
|
-
const { scanKotlinDomains, resolveSharedQueryDomains } = require("./scanners/scan-kotlin");
|
|
17
|
-
const { scanNodeDomains } = require("./scanners/scan-node");
|
|
18
|
-
const { scanPythonDomains } = require("./scanners/scan-python");
|
|
19
|
-
const { scanFrontendDomains, countFrontendStats } = require("./scanners/scan-frontend");
|
|
20
|
-
|
|
21
|
-
async function scanStructure(stack, ROOT) {
|
|
22
|
-
let backendDomains = [];
|
|
23
|
-
let frontendDomains = [];
|
|
24
|
-
let rootPackage = null;
|
|
25
|
-
|
|
26
|
-
// ── Backend scanners ──
|
|
27
|
-
if (stack.language === "java") {
|
|
28
|
-
const r = await scanJavaDomains(stack, ROOT);
|
|
29
|
-
backendDomains.push(...r.backendDomains);
|
|
30
|
-
if (r.rootPackage) rootPackage = r.rootPackage;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (stack.language === "kotlin") {
|
|
34
|
-
const r = await scanKotlinDomains(stack, ROOT);
|
|
35
|
-
backendDomains.push(...r.backendDomains);
|
|
36
|
-
if (r.rootPackage) rootPackage = r.rootPackage;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework && stack.framework !== "vite") {
|
|
40
|
-
const r = await scanNodeDomains(stack, ROOT);
|
|
41
|
-
backendDomains.push(...r.backendDomains);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (stack.language === "python") {
|
|
45
|
-
const r = await scanPythonDomains(stack, ROOT);
|
|
46
|
-
backendDomains.push(...r.backendDomains);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── Frontend scanner ──
|
|
50
|
-
const fe = await scanFrontendDomains(stack, ROOT);
|
|
51
|
-
frontendDomains.push(...fe.frontendDomains);
|
|
52
|
-
|
|
53
|
-
// ── Frontend stats ──
|
|
54
|
-
const frontend = await countFrontendStats(stack, ROOT);
|
|
55
|
-
|
|
56
|
-
// ── Aggregate ──
|
|
57
|
-
const allDomains = [
|
|
58
|
-
...backendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
59
|
-
...frontendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
return { domains: allDomains, backendDomains, frontendDomains, rootPackage, frontend };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
module.exports = { scanStructure, resolveSharedQueryDomains };
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Structure Scanner (Orchestrator)
|
|
3
|
+
*
|
|
4
|
+
* Scans project directory structure to discover domains (backend + frontend).
|
|
5
|
+
* Delegates to language-specific scanners in ./scanners/.
|
|
6
|
+
*
|
|
7
|
+
* Supported scanners:
|
|
8
|
+
* - scan-java.js — Java (5 patterns: A/B/C/D/E + fallback)
|
|
9
|
+
* - scan-kotlin.js — Kotlin (multi-module + CQRS + single fallback)
|
|
10
|
+
* - scan-node.js — Node.js (Express/NestJS/Fastify)
|
|
11
|
+
* - scan-python.js — Python (Django/FastAPI/Flask)
|
|
12
|
+
* - scan-frontend.js — Angular, Next.js, React, Vue (+ 4-stage fallback + stats)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { scanJavaDomains } = require("./scanners/scan-java");
|
|
16
|
+
const { scanKotlinDomains, resolveSharedQueryDomains } = require("./scanners/scan-kotlin");
|
|
17
|
+
const { scanNodeDomains } = require("./scanners/scan-node");
|
|
18
|
+
const { scanPythonDomains } = require("./scanners/scan-python");
|
|
19
|
+
const { scanFrontendDomains, countFrontendStats } = require("./scanners/scan-frontend");
|
|
20
|
+
|
|
21
|
+
async function scanStructure(stack, ROOT) {
|
|
22
|
+
let backendDomains = [];
|
|
23
|
+
let frontendDomains = [];
|
|
24
|
+
let rootPackage = null;
|
|
25
|
+
|
|
26
|
+
// ── Backend scanners ──
|
|
27
|
+
if (stack.language === "java") {
|
|
28
|
+
const r = await scanJavaDomains(stack, ROOT);
|
|
29
|
+
backendDomains.push(...r.backendDomains);
|
|
30
|
+
if (r.rootPackage) rootPackage = r.rootPackage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (stack.language === "kotlin") {
|
|
34
|
+
const r = await scanKotlinDomains(stack, ROOT);
|
|
35
|
+
backendDomains.push(...r.backendDomains);
|
|
36
|
+
if (r.rootPackage) rootPackage = r.rootPackage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if ((stack.language === "typescript" || stack.language === "javascript") && stack.framework && stack.framework !== "vite") {
|
|
40
|
+
const r = await scanNodeDomains(stack, ROOT);
|
|
41
|
+
backendDomains.push(...r.backendDomains);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (stack.language === "python") {
|
|
45
|
+
const r = await scanPythonDomains(stack, ROOT);
|
|
46
|
+
backendDomains.push(...r.backendDomains);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Frontend scanner ──
|
|
50
|
+
const fe = await scanFrontendDomains(stack, ROOT);
|
|
51
|
+
frontendDomains.push(...fe.frontendDomains);
|
|
52
|
+
|
|
53
|
+
// ── Frontend stats ──
|
|
54
|
+
const frontend = await countFrontendStats(stack, ROOT);
|
|
55
|
+
|
|
56
|
+
// ── Aggregate ──
|
|
57
|
+
const allDomains = [
|
|
58
|
+
...backendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
59
|
+
...frontendDomains.sort((a, b) => b.totalFiles - a.totalFiles),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
return { domains: allDomains, backendDomains, frontendDomains, rootPackage, frontend };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { scanStructure, resolveSharedQueryDomains };
|
package/sync-checker/index.js
CHANGED
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ClaudeOS-Core — Sync Checker
|
|
5
|
-
*
|
|
6
|
-
* Role: Check disk ↔ Master Plan sync status based on sync-map.json
|
|
7
|
-
* Detection items:
|
|
8
|
-
* - Unregistered: file exists on disk but not registered in any plan
|
|
9
|
-
* - Orphaned: registered in plan but missing from disk
|
|
10
|
-
*
|
|
11
|
-
* Usage: npx claudeos-core <cmd> or node claudeos-core-tools/sync-checker/index.js
|
|
12
|
-
* Depends: manifest-generator must run first (sync-map.json)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const fs = require("fs");
|
|
16
|
-
const path = require("path");
|
|
17
|
-
const { glob } = require("glob");
|
|
18
|
-
const { updateStaleReport } = require("../lib/stale-report");
|
|
19
|
-
|
|
20
|
-
const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
|
|
21
|
-
const GEN = path.join(ROOT, "claudeos-core/generated");
|
|
22
|
-
const SMP = path.join(GEN, "sync-map.json");
|
|
23
|
-
|
|
24
|
-
const TRACKED = [
|
|
25
|
-
{ dir: ".claude/rules", pfx: "rules" },
|
|
26
|
-
{ dir: "claudeos-core/standard", pfx: "standard" },
|
|
27
|
-
{ dir: "claudeos-core/skills", pfx: "skills" },
|
|
28
|
-
{ dir: "claudeos-core/guide", pfx: "guide" },
|
|
29
|
-
{ dir: "claudeos-core/database", pfx: "database" },
|
|
30
|
-
{ dir: "claudeos-core/mcp-guide", pfx: "mcp-guide" },
|
|
31
|
-
{ dir: "claudeos-core/memory", pfx: "memory" },
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
function rel(p) {
|
|
35
|
-
return path.relative(ROOT, p).replace(/\\/g, "/");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isWithinRoot(absPath) {
|
|
39
|
-
let resolved = path.resolve(absPath);
|
|
40
|
-
let root = path.resolve(ROOT);
|
|
41
|
-
if (process.platform === "win32") {
|
|
42
|
-
resolved = resolved.toLowerCase();
|
|
43
|
-
root = root.toLowerCase();
|
|
44
|
-
}
|
|
45
|
-
return resolved === root || resolved.startsWith(root + path.sep);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function main() {
|
|
49
|
-
console.log("\n╔═══════════════════════════════════════╗");
|
|
50
|
-
console.log("║ ClaudeOS-Core — Sync Checker ║");
|
|
51
|
-
console.log("╚═══════════════════════════════════════╝\n");
|
|
52
|
-
|
|
53
|
-
// Master plan directory is optional. If it doesn't exist (new default for
|
|
54
|
-
// claudeos-core, since master plans are no longer generated) AND sync-map
|
|
55
|
-
// has no mappings to validate, sync-checker has nothing to compare against
|
|
56
|
-
// and should skip cleanly. This is a PASS state, not a failure.
|
|
57
|
-
//
|
|
58
|
-
// However, if sync-map.json DOES contain mappings (either because master
|
|
59
|
-
// plans exist, or because a caller wrote mappings directly for testing),
|
|
60
|
-
// we still validate them normally.
|
|
61
|
-
const PLAN_DIR = path.join(ROOT, "claudeos-core/plan");
|
|
62
|
-
const planExists = fs.existsSync(PLAN_DIR);
|
|
63
|
-
|
|
64
|
-
if (!fs.existsSync(SMP)) {
|
|
65
|
-
// No sync-map at all.
|
|
66
|
-
if (!planExists) {
|
|
67
|
-
console.log(" ℹ️ No plan/ directory and no sync-map.json — nothing to compare; skipping.\n");
|
|
68
|
-
updateStaleReport(GEN, "syncMisses",
|
|
69
|
-
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
70
|
-
{ syncIssues: 0, status: "ok" }
|
|
71
|
-
);
|
|
72
|
-
process.exit(0);
|
|
73
|
-
}
|
|
74
|
-
console.log(" ❌ sync-map.json not found. Run manifest-generator first.\n");
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let sm;
|
|
79
|
-
try {
|
|
80
|
-
sm = JSON.parse(fs.readFileSync(SMP, "utf-8"));
|
|
81
|
-
} catch (e) {
|
|
82
|
-
console.log(` ❌ sync-map.json is malformed: ${e.message}\n`);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
if (!Array.isArray(sm.mappings)) {
|
|
86
|
-
console.log(" ❌ sync-map.json has no mappings array.\n");
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// If sync-map has no mappings AND plan/ directory doesn't exist, skip
|
|
91
|
-
// cleanly — there's no ground truth to validate against and no master plans
|
|
92
|
-
// in use.
|
|
93
|
-
if (sm.mappings.length === 0 && !planExists) {
|
|
94
|
-
console.log(" ℹ️ No plan/ directory and sync-map has no mappings — skipping.\n");
|
|
95
|
-
updateStaleReport(GEN, "syncMisses",
|
|
96
|
-
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
97
|
-
{ syncIssues: 0, status: "ok" }
|
|
98
|
-
);
|
|
99
|
-
process.exit(0);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// If sync-map has no mappings but plan/ exists (e.g., empty plan files),
|
|
103
|
-
// skip without raising a warning — there's nothing to validate.
|
|
104
|
-
if (sm.mappings.length === 0) {
|
|
105
|
-
console.log(" ℹ️ sync-map has no mappings — nothing to validate; skipping.\n");
|
|
106
|
-
updateStaleReport(GEN, "syncMisses",
|
|
107
|
-
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
108
|
-
{ syncIssues: 0, status: "ok" }
|
|
109
|
-
);
|
|
110
|
-
process.exit(0);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const reg = new Set(sm.mappings.map((m) => m.sourcePath).filter(Boolean));
|
|
114
|
-
const issues = { unreg: [], orphan: [] };
|
|
115
|
-
|
|
116
|
-
// ─── [1/2] Disk → Plan: detect unregistered files ───────
|
|
117
|
-
console.log(" [1/2] Disk → Plan...");
|
|
118
|
-
for (const t of TRACKED) {
|
|
119
|
-
const abs = path.join(ROOT, t.dir);
|
|
120
|
-
if (!fs.existsSync(abs)) continue;
|
|
121
|
-
|
|
122
|
-
for (const f of await glob("**/*.md", { cwd: abs, absolute: true })) {
|
|
123
|
-
const r = rel(f);
|
|
124
|
-
if (path.basename(f) === "README.md") continue;
|
|
125
|
-
if (!reg.has(r)) {
|
|
126
|
-
issues.unreg.push({ path: r, domain: t.pfx });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Check CLAUDE.md separately
|
|
132
|
-
if (fs.existsSync(path.join(ROOT, "CLAUDE.md")) && !reg.has("CLAUDE.md")) {
|
|
133
|
-
issues.unreg.push({ path: "CLAUDE.md", domain: "root" });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ─── [2/2] Plan → Disk: detect orphaned files ───────────────
|
|
137
|
-
console.log(" [2/2] Plan → Disk...");
|
|
138
|
-
for (const m of sm.mappings) {
|
|
139
|
-
if (!m.sourcePath) continue;
|
|
140
|
-
const abs = path.join(ROOT, m.sourcePath);
|
|
141
|
-
// Skip path traversal attempts (allow files at ROOT level and below)
|
|
142
|
-
if (!isWithinRoot(abs)) continue;
|
|
143
|
-
if (!fs.existsSync(abs)) {
|
|
144
|
-
issues.orphan.push({ path: m.sourcePath, plan: m.planFile });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ─── Output results ─────────────────────────────────────────
|
|
149
|
-
if (issues.unreg.length) {
|
|
150
|
-
console.log(`\n ⚠️ Unregistered (${issues.unreg.length}):`);
|
|
151
|
-
issues.unreg.forEach((i) => console.log(` + ${i.path}`));
|
|
152
|
-
}
|
|
153
|
-
if (issues.orphan.length) {
|
|
154
|
-
console.log(`\n ⚠️ Orphaned (${issues.orphan.length}):`);
|
|
155
|
-
issues.orphan.forEach((i) => console.log(` - ${i.path}`));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const total = issues.unreg.length + issues.orphan.length;
|
|
159
|
-
console.log(`\n Registered: ${reg.size} | Unregistered: ${issues.unreg.length} | Orphaned: ${issues.orphan.length}`);
|
|
160
|
-
console.log(total === 0 ? " ✅ All in sync\n" : ` ⚠️ ${total} issues\n`);
|
|
161
|
-
|
|
162
|
-
// ─── Update stale-report.json ────────────────────────────
|
|
163
|
-
updateStaleReport(GEN, "syncMisses",
|
|
164
|
-
{ checkedAt: new Date().toISOString(), unregistered: issues.unreg, orphaned: issues.orphan },
|
|
165
|
-
{ syncIssues: total, status: total === 0 ? "ok" : "warning" }
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// Exit 1 only for orphaned files (actual breakage), not for unregistered (informational)
|
|
169
|
-
const orphanCount = issues.orphan.length;
|
|
170
|
-
process.exit(orphanCount > 0 ? 1 : 0);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (require.main === module) {
|
|
174
|
-
main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
module.exports = { main };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ClaudeOS-Core — Sync Checker
|
|
5
|
+
*
|
|
6
|
+
* Role: Check disk ↔ Master Plan sync status based on sync-map.json
|
|
7
|
+
* Detection items:
|
|
8
|
+
* - Unregistered: file exists on disk but not registered in any plan
|
|
9
|
+
* - Orphaned: registered in plan but missing from disk
|
|
10
|
+
*
|
|
11
|
+
* Usage: npx claudeos-core <cmd> or node claudeos-core-tools/sync-checker/index.js
|
|
12
|
+
* Depends: manifest-generator must run first (sync-map.json)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const { glob } = require("glob");
|
|
18
|
+
const { updateStaleReport } = require("../lib/stale-report");
|
|
19
|
+
|
|
20
|
+
const ROOT = process.env.CLAUDEOS_ROOT || path.resolve(__dirname, "../..");
|
|
21
|
+
const GEN = path.join(ROOT, "claudeos-core/generated");
|
|
22
|
+
const SMP = path.join(GEN, "sync-map.json");
|
|
23
|
+
|
|
24
|
+
const TRACKED = [
|
|
25
|
+
{ dir: ".claude/rules", pfx: "rules" },
|
|
26
|
+
{ dir: "claudeos-core/standard", pfx: "standard" },
|
|
27
|
+
{ dir: "claudeos-core/skills", pfx: "skills" },
|
|
28
|
+
{ dir: "claudeos-core/guide", pfx: "guide" },
|
|
29
|
+
{ dir: "claudeos-core/database", pfx: "database" },
|
|
30
|
+
{ dir: "claudeos-core/mcp-guide", pfx: "mcp-guide" },
|
|
31
|
+
{ dir: "claudeos-core/memory", pfx: "memory" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function rel(p) {
|
|
35
|
+
return path.relative(ROOT, p).replace(/\\/g, "/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isWithinRoot(absPath) {
|
|
39
|
+
let resolved = path.resolve(absPath);
|
|
40
|
+
let root = path.resolve(ROOT);
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
resolved = resolved.toLowerCase();
|
|
43
|
+
root = root.toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
return resolved === root || resolved.startsWith(root + path.sep);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
console.log("\n╔═══════════════════════════════════════╗");
|
|
50
|
+
console.log("║ ClaudeOS-Core — Sync Checker ║");
|
|
51
|
+
console.log("╚═══════════════════════════════════════╝\n");
|
|
52
|
+
|
|
53
|
+
// Master plan directory is optional. If it doesn't exist (new default for
|
|
54
|
+
// claudeos-core, since master plans are no longer generated) AND sync-map
|
|
55
|
+
// has no mappings to validate, sync-checker has nothing to compare against
|
|
56
|
+
// and should skip cleanly. This is a PASS state, not a failure.
|
|
57
|
+
//
|
|
58
|
+
// However, if sync-map.json DOES contain mappings (either because master
|
|
59
|
+
// plans exist, or because a caller wrote mappings directly for testing),
|
|
60
|
+
// we still validate them normally.
|
|
61
|
+
const PLAN_DIR = path.join(ROOT, "claudeos-core/plan");
|
|
62
|
+
const planExists = fs.existsSync(PLAN_DIR);
|
|
63
|
+
|
|
64
|
+
if (!fs.existsSync(SMP)) {
|
|
65
|
+
// No sync-map at all.
|
|
66
|
+
if (!planExists) {
|
|
67
|
+
console.log(" ℹ️ No plan/ directory and no sync-map.json — nothing to compare; skipping.\n");
|
|
68
|
+
updateStaleReport(GEN, "syncMisses",
|
|
69
|
+
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
70
|
+
{ syncIssues: 0, status: "ok" }
|
|
71
|
+
);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
console.log(" ❌ sync-map.json not found. Run manifest-generator first.\n");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let sm;
|
|
79
|
+
try {
|
|
80
|
+
sm = JSON.parse(fs.readFileSync(SMP, "utf-8"));
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.log(` ❌ sync-map.json is malformed: ${e.message}\n`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(sm.mappings)) {
|
|
86
|
+
console.log(" ❌ sync-map.json has no mappings array.\n");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If sync-map has no mappings AND plan/ directory doesn't exist, skip
|
|
91
|
+
// cleanly — there's no ground truth to validate against and no master plans
|
|
92
|
+
// in use.
|
|
93
|
+
if (sm.mappings.length === 0 && !planExists) {
|
|
94
|
+
console.log(" ℹ️ No plan/ directory and sync-map has no mappings — skipping.\n");
|
|
95
|
+
updateStaleReport(GEN, "syncMisses",
|
|
96
|
+
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
97
|
+
{ syncIssues: 0, status: "ok" }
|
|
98
|
+
);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If sync-map has no mappings but plan/ exists (e.g., empty plan files),
|
|
103
|
+
// skip without raising a warning — there's nothing to validate.
|
|
104
|
+
if (sm.mappings.length === 0) {
|
|
105
|
+
console.log(" ℹ️ sync-map has no mappings — nothing to validate; skipping.\n");
|
|
106
|
+
updateStaleReport(GEN, "syncMisses",
|
|
107
|
+
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
108
|
+
{ syncIssues: 0, status: "ok" }
|
|
109
|
+
);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const reg = new Set(sm.mappings.map((m) => m.sourcePath).filter(Boolean));
|
|
114
|
+
const issues = { unreg: [], orphan: [] };
|
|
115
|
+
|
|
116
|
+
// ─── [1/2] Disk → Plan: detect unregistered files ───────
|
|
117
|
+
console.log(" [1/2] Disk → Plan...");
|
|
118
|
+
for (const t of TRACKED) {
|
|
119
|
+
const abs = path.join(ROOT, t.dir);
|
|
120
|
+
if (!fs.existsSync(abs)) continue;
|
|
121
|
+
|
|
122
|
+
for (const f of await glob("**/*.md", { cwd: abs, absolute: true })) {
|
|
123
|
+
const r = rel(f);
|
|
124
|
+
if (path.basename(f) === "README.md") continue;
|
|
125
|
+
if (!reg.has(r)) {
|
|
126
|
+
issues.unreg.push({ path: r, domain: t.pfx });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check CLAUDE.md separately
|
|
132
|
+
if (fs.existsSync(path.join(ROOT, "CLAUDE.md")) && !reg.has("CLAUDE.md")) {
|
|
133
|
+
issues.unreg.push({ path: "CLAUDE.md", domain: "root" });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── [2/2] Plan → Disk: detect orphaned files ───────────────
|
|
137
|
+
console.log(" [2/2] Plan → Disk...");
|
|
138
|
+
for (const m of sm.mappings) {
|
|
139
|
+
if (!m.sourcePath) continue;
|
|
140
|
+
const abs = path.join(ROOT, m.sourcePath);
|
|
141
|
+
// Skip path traversal attempts (allow files at ROOT level and below)
|
|
142
|
+
if (!isWithinRoot(abs)) continue;
|
|
143
|
+
if (!fs.existsSync(abs)) {
|
|
144
|
+
issues.orphan.push({ path: m.sourcePath, plan: m.planFile });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Output results ─────────────────────────────────────────
|
|
149
|
+
if (issues.unreg.length) {
|
|
150
|
+
console.log(`\n ⚠️ Unregistered (${issues.unreg.length}):`);
|
|
151
|
+
issues.unreg.forEach((i) => console.log(` + ${i.path}`));
|
|
152
|
+
}
|
|
153
|
+
if (issues.orphan.length) {
|
|
154
|
+
console.log(`\n ⚠️ Orphaned (${issues.orphan.length}):`);
|
|
155
|
+
issues.orphan.forEach((i) => console.log(` - ${i.path}`));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const total = issues.unreg.length + issues.orphan.length;
|
|
159
|
+
console.log(`\n Registered: ${reg.size} | Unregistered: ${issues.unreg.length} | Orphaned: ${issues.orphan.length}`);
|
|
160
|
+
console.log(total === 0 ? " ✅ All in sync\n" : ` ⚠️ ${total} issues\n`);
|
|
161
|
+
|
|
162
|
+
// ─── Update stale-report.json ────────────────────────────
|
|
163
|
+
updateStaleReport(GEN, "syncMisses",
|
|
164
|
+
{ checkedAt: new Date().toISOString(), unregistered: issues.unreg, orphaned: issues.orphan },
|
|
165
|
+
{ syncIssues: total, status: total === 0 ? "ok" : "warning" }
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Exit 1 only for orphaned files (actual breakage), not for unregistered (informational)
|
|
169
|
+
const orphanCount = issues.orphan.length;
|
|
170
|
+
process.exit(orphanCount > 0 ? 1 : 0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (require.main === module) {
|
|
174
|
+
main().catch(e => { console.error(`\n ❌ Unexpected error: ${e.message || e}`); process.exit(1); });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { main };
|