claudeos-core 2.2.0 → 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 (49) hide show
  1. package/CHANGELOG.md +1649 -907
  2. package/CONTRIBUTING.md +92 -92
  3. package/README.de.md +32 -0
  4. package/README.es.md +32 -0
  5. package/README.fr.md +32 -0
  6. package/README.hi.md +32 -0
  7. package/README.ja.md +32 -0
  8. package/README.ko.md +1018 -986
  9. package/README.md +1020 -987
  10. package/README.ru.md +32 -0
  11. package/README.vi.md +1019 -987
  12. package/README.zh-CN.md +32 -0
  13. package/bin/cli.js +152 -148
  14. package/bin/commands/init.js +1673 -1554
  15. package/bin/commands/lint.js +62 -0
  16. package/bin/commands/memory.js +438 -438
  17. package/bin/lib/cli-utils.js +206 -206
  18. package/claude-md-validator/index.js +184 -0
  19. package/claude-md-validator/reporter.js +66 -0
  20. package/claude-md-validator/structural-checks.js +528 -0
  21. package/content-validator/index.js +666 -441
  22. package/lib/expected-guides.js +23 -23
  23. package/lib/expected-outputs.js +90 -90
  24. package/lib/language-config.js +35 -35
  25. package/lib/memory-scaffold.js +1058 -1054
  26. package/lib/plan-parser.js +165 -165
  27. package/lib/staged-rules.js +118 -118
  28. package/manifest-generator/index.js +174 -174
  29. package/package.json +90 -87
  30. package/pass-json-validator/index.js +337 -337
  31. package/pass-prompts/templates/common/claude-md-scaffold.md +52 -10
  32. package/pass-prompts/templates/common/pass3-footer.md +402 -224
  33. package/pass-prompts/templates/common/pass3b-core-header.md +43 -0
  34. package/pass-prompts/templates/common/pass4.md +375 -305
  35. package/pass-prompts/templates/common/staging-override.md +26 -26
  36. package/pass-prompts/templates/node-vite/pass1.md +117 -117
  37. package/pass-prompts/templates/node-vite/pass2.md +78 -78
  38. package/pass-prompts/templates/python-flask/pass1.md +119 -119
  39. package/pass-prompts/templates/python-flask/pass2.md +85 -85
  40. package/plan-installer/domain-grouper.js +76 -76
  41. package/plan-installer/index.js +137 -137
  42. package/plan-installer/prompt-generator.js +188 -145
  43. package/plan-installer/scanners/scan-frontend.js +505 -473
  44. package/plan-installer/scanners/scan-java.js +226 -226
  45. package/plan-installer/scanners/scan-node.js +57 -57
  46. package/plan-installer/scanners/scan-python.js +85 -85
  47. package/plan-installer/stack-detector.js +482 -482
  48. package/plan-installer/structure-scanner.js +65 -65
  49. package/sync-checker/index.js +177 -177
@@ -1,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 };
@@ -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 };