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,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 };