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,206 +1,206 @@
1
- /**
2
- * ClaudeOS-Core — CLI Utilities
3
- *
4
- * Shared constants, execution helpers, and filesystem utilities for the CLI.
5
- */
6
-
7
- const { execSync, spawn } = require("child_process");
8
- const fs = require("fs");
9
- const path = require("path");
10
- const { ensureDir, existsSafe } = require("../../lib/safe-fs");
11
-
12
- // ─── Path configuration ──────────────────────────────────────────
13
- const TOOLS_DIR = path.resolve(__dirname, "../..");
14
- const PROJECT_ROOT = process.cwd();
15
- const GENERATED_DIR = path.join(PROJECT_ROOT, "claudeos-core/generated");
16
-
17
- // ─── Language configuration ──────────────────────────────────────
18
- // Single source of truth: lib/language-config.js. We re-export under the
19
- // historical name SUPPORTED_LANGS so existing imports keep working.
20
- const { LANGUAGES: SUPPORTED_LANGS, LANG_CODES, isValidLang } = require("../../lib/language-config");
21
-
22
- // ─── Output ─────────────────────────────────────────────────────
23
- function log(msg) {
24
- console.log(msg);
25
- }
26
-
27
- function header(title) {
28
- log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
29
- log(title);
30
- log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
31
- }
32
-
33
- // ─── Execution ──────────────────────────────────────────────────
34
- function run(cmd, options = {}) {
35
- try {
36
- execSync(cmd, {
37
- cwd: options.cwd || PROJECT_ROOT,
38
- stdio: options.silent ? ["pipe", "pipe", "pipe"] : "inherit",
39
- encoding: "utf-8",
40
- timeout: options.timeout || 0,
41
- });
42
- return true;
43
- } catch (e) {
44
- if (options.ignoreError) return false;
45
- throw e;
46
- }
47
- }
48
-
49
- // Run claude -p: pass prompt via stdin (no shell pipe — avoids command injection)
50
- function runClaudePrompt(prompt, options = {}) {
51
- try {
52
- execSync("claude -p --dangerously-skip-permissions", {
53
- input: prompt,
54
- cwd: options.cwd || PROJECT_ROOT,
55
- stdio: ["pipe", "inherit", "inherit"],
56
- encoding: "utf-8",
57
- timeout: 0,
58
- });
59
- return true;
60
- } catch (e) {
61
- if (options.ignoreError) return false;
62
- throw e;
63
- }
64
- }
65
-
66
- // Async variant of runClaudePrompt using spawn — does NOT block the event loop,
67
- // so the caller can run setInterval-based progress callbacks concurrently.
68
- // Resolves to true on exit code 0, false otherwise (no throw; mirrors ignoreError:true).
69
- // onTick is invoked every tickMs while claude is running; stopped on close.
70
- // shell:true on Windows so that `claude.cmd`/`claude.ps1` shims resolve via PATH.
71
- function runClaudePromptAsync(prompt, options = {}) {
72
- return new Promise((resolve) => {
73
- let child = null;
74
- let tickTimer = null;
75
- const cleanup = () => { if (tickTimer) { clearInterval(tickTimer); tickTimer = null; } };
76
- const bail = () => {
77
- cleanup();
78
- // Best-effort kill so we don't leave orphaned claude processes when we
79
- // fail to hand off the prompt or encounter an unexpected spawn error.
80
- if (child && !child.killed) { try { child.kill(); } catch (_e) { /* ignore */ } }
81
- resolve(false);
82
- };
83
- try {
84
- // Node 18+ emits DEP0190 when mixing shell:true with an args array (the
85
- // args aren't escaped — just concatenated). On Windows we need shell:true
86
- // so `claude.cmd`/`claude.ps1` shims resolve via PATH, so we build the
87
- // whole command as a single string there and pass an empty args array.
88
- // The flags are hardcoded literals (no user input) so there's no
89
- // injection surface either way; this just silences the warning.
90
- const isWin = process.platform === "win32";
91
- const spawnCmd = isWin ? "claude -p --dangerously-skip-permissions" : "claude";
92
- const spawnArgs = isWin ? [] : ["-p", "--dangerously-skip-permissions"];
93
- child = spawn(spawnCmd, spawnArgs, {
94
- cwd: options.cwd || PROJECT_ROOT,
95
- stdio: ["pipe", "inherit", "inherit"],
96
- shell: isWin,
97
- });
98
- if (typeof options.onTick === "function" && options.tickMs > 0) {
99
- // Fire once immediately so the user sees the progress line right away,
100
- // instead of waiting a full tick interval for any feedback.
101
- try { options.onTick(); } catch (_e) { /* swallow */ }
102
- tickTimer = setInterval(() => {
103
- try { options.onTick(); } catch (_e) { /* swallow — progress is best-effort */ }
104
- }, options.tickMs);
105
- }
106
- child.on("close", (code) => { cleanup(); resolve(code === 0); });
107
- child.on("error", () => { cleanup(); resolve(false); });
108
- child.stdin.on("error", () => { bail(); });
109
- child.stdin.write(prompt);
110
- child.stdin.end();
111
- } catch (_e) {
112
- // Catches synchronous throws from spawn (e.g. ENAMETOOLONG on some
113
- // platforms) or from the initial stdin.write before listeners were
114
- // attached. Either way: no orphan child, no leaked interval.
115
- bail();
116
- }
117
- });
118
- }
119
-
120
- // Run claude -p but CAPTURE stdout instead of inheriting it.
121
- // Returns the captured stdout string on success, or null on failure.
122
- // Used for short tasks where we need the response content (e.g. translation).
123
- // maxBuffer default is 10MB — enough for document translation.
124
- function runClaudeCapture(prompt, options = {}) {
125
- try {
126
- const out = execSync("claude -p --dangerously-skip-permissions", {
127
- input: prompt,
128
- cwd: options.cwd || PROJECT_ROOT,
129
- stdio: ["pipe", "pipe", "pipe"],
130
- encoding: "utf-8",
131
- timeout: options.timeout || 0,
132
- maxBuffer: options.maxBuffer || 10 * 1024 * 1024,
133
- });
134
- return out;
135
- } catch (_e) {
136
- return null;
137
- }
138
- }
139
-
140
- // ─── Filesystem ─────────────────────────────────────────────────
141
- // ensureDir: delegated to lib/safe-fs.js (single source of truth)
142
- // fileExists: alias for existsSafe from lib/safe-fs.js
143
- const fileExists = existsSafe;
144
-
145
- // readFile: intentionally throws on error (CLI needs hard failure, unlike safe-fs fallback)
146
- function readFile(p) {
147
- return fs.readFileSync(p, "utf-8");
148
- }
149
-
150
- function injectProjectRoot(text) {
151
- // Normalize to forward slashes for prompts (Claude interprets backslashes as escapes)
152
- const normalizedRoot = PROJECT_ROOT.replace(/\\/g, "/");
153
- // Use a replacement function so that `$`, `$1`, `$&`, `$$` in the
154
- // project path (rare but possible on some systems) are preserved as
155
- // literal characters rather than interpreted as regex specials.
156
- return text.replace(/\{\{PROJECT_ROOT\}\}/g, () => normalizedRoot);
157
- }
158
-
159
- // ─── Helpers ────────────────────────────────────────────────────
160
- function pad(str, len) {
161
- return str.length >= len ? str : str + " ".repeat(len - str.length);
162
- }
163
-
164
- function countFiles() {
165
- try {
166
- let count = 0;
167
- const skipDirs = ["node_modules", "generated"];
168
- const visited = new Set();
169
- const scan = (dir) => {
170
- if (!fs.existsSync(dir)) return;
171
- let realDir;
172
- try { realDir = fs.realpathSync(dir); } catch (_e) { realDir = dir; }
173
- if (visited.has(realDir)) return;
174
- visited.add(realDir);
175
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
176
- if (skipDirs.includes(entry.name)) continue;
177
- const full = path.join(dir, entry.name);
178
- if (entry.isDirectory()) scan(full);
179
- else count++;
180
- }
181
- };
182
- scan(path.join(PROJECT_ROOT, ".claude"));
183
- scan(path.join(PROJECT_ROOT, "claudeos-core"));
184
- return count;
185
- } catch (e) {
186
- return "?";
187
- }
188
- }
189
-
190
- function countPass1Files() {
191
- try {
192
- return fs
193
- .readdirSync(GENERATED_DIR)
194
- .filter((f) => f.startsWith("pass1-") && f.endsWith(".json")).length;
195
- } catch (e) {
196
- return 0;
197
- }
198
- }
199
-
200
- module.exports = {
201
- TOOLS_DIR, PROJECT_ROOT, GENERATED_DIR,
202
- SUPPORTED_LANGS, LANG_CODES, isValidLang,
203
- log, header, run, runClaudePrompt, runClaudePromptAsync, runClaudeCapture,
204
- ensureDir, fileExists, readFile, injectProjectRoot,
205
- pad, countFiles, countPass1Files,
206
- };
1
+ /**
2
+ * ClaudeOS-Core — CLI Utilities
3
+ *
4
+ * Shared constants, execution helpers, and filesystem utilities for the CLI.
5
+ */
6
+
7
+ const { execSync, spawn } = require("child_process");
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const { ensureDir, existsSafe } = require("../../lib/safe-fs");
11
+
12
+ // ─── Path configuration ──────────────────────────────────────────
13
+ const TOOLS_DIR = path.resolve(__dirname, "../..");
14
+ const PROJECT_ROOT = process.cwd();
15
+ const GENERATED_DIR = path.join(PROJECT_ROOT, "claudeos-core/generated");
16
+
17
+ // ─── Language configuration ──────────────────────────────────────
18
+ // Single source of truth: lib/language-config.js. We re-export under the
19
+ // historical name SUPPORTED_LANGS so existing imports keep working.
20
+ const { LANGUAGES: SUPPORTED_LANGS, LANG_CODES, isValidLang } = require("../../lib/language-config");
21
+
22
+ // ─── Output ─────────────────────────────────────────────────────
23
+ function log(msg) {
24
+ console.log(msg);
25
+ }
26
+
27
+ function header(title) {
28
+ log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
29
+ log(title);
30
+ log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
31
+ }
32
+
33
+ // ─── Execution ──────────────────────────────────────────────────
34
+ function run(cmd, options = {}) {
35
+ try {
36
+ execSync(cmd, {
37
+ cwd: options.cwd || PROJECT_ROOT,
38
+ stdio: options.silent ? ["pipe", "pipe", "pipe"] : "inherit",
39
+ encoding: "utf-8",
40
+ timeout: options.timeout || 0,
41
+ });
42
+ return true;
43
+ } catch (e) {
44
+ if (options.ignoreError) return false;
45
+ throw e;
46
+ }
47
+ }
48
+
49
+ // Run claude -p: pass prompt via stdin (no shell pipe — avoids command injection)
50
+ function runClaudePrompt(prompt, options = {}) {
51
+ try {
52
+ execSync("claude -p --dangerously-skip-permissions", {
53
+ input: prompt,
54
+ cwd: options.cwd || PROJECT_ROOT,
55
+ stdio: ["pipe", "inherit", "inherit"],
56
+ encoding: "utf-8",
57
+ timeout: 0,
58
+ });
59
+ return true;
60
+ } catch (e) {
61
+ if (options.ignoreError) return false;
62
+ throw e;
63
+ }
64
+ }
65
+
66
+ // Async variant of runClaudePrompt using spawn — does NOT block the event loop,
67
+ // so the caller can run setInterval-based progress callbacks concurrently.
68
+ // Resolves to true on exit code 0, false otherwise (no throw; mirrors ignoreError:true).
69
+ // onTick is invoked every tickMs while claude is running; stopped on close.
70
+ // shell:true on Windows so that `claude.cmd`/`claude.ps1` shims resolve via PATH.
71
+ function runClaudePromptAsync(prompt, options = {}) {
72
+ return new Promise((resolve) => {
73
+ let child = null;
74
+ let tickTimer = null;
75
+ const cleanup = () => { if (tickTimer) { clearInterval(tickTimer); tickTimer = null; } };
76
+ const bail = () => {
77
+ cleanup();
78
+ // Best-effort kill so we don't leave orphaned claude processes when we
79
+ // fail to hand off the prompt or encounter an unexpected spawn error.
80
+ if (child && !child.killed) { try { child.kill(); } catch (_e) { /* ignore */ } }
81
+ resolve(false);
82
+ };
83
+ try {
84
+ // Node 18+ emits DEP0190 when mixing shell:true with an args array (the
85
+ // args aren't escaped — just concatenated). On Windows we need shell:true
86
+ // so `claude.cmd`/`claude.ps1` shims resolve via PATH, so we build the
87
+ // whole command as a single string there and pass an empty args array.
88
+ // The flags are hardcoded literals (no user input) so there's no
89
+ // injection surface either way; this just silences the warning.
90
+ const isWin = process.platform === "win32";
91
+ const spawnCmd = isWin ? "claude -p --dangerously-skip-permissions" : "claude";
92
+ const spawnArgs = isWin ? [] : ["-p", "--dangerously-skip-permissions"];
93
+ child = spawn(spawnCmd, spawnArgs, {
94
+ cwd: options.cwd || PROJECT_ROOT,
95
+ stdio: ["pipe", "inherit", "inherit"],
96
+ shell: isWin,
97
+ });
98
+ if (typeof options.onTick === "function" && options.tickMs > 0) {
99
+ // Fire once immediately so the user sees the progress line right away,
100
+ // instead of waiting a full tick interval for any feedback.
101
+ try { options.onTick(); } catch (_e) { /* swallow */ }
102
+ tickTimer = setInterval(() => {
103
+ try { options.onTick(); } catch (_e) { /* swallow — progress is best-effort */ }
104
+ }, options.tickMs);
105
+ }
106
+ child.on("close", (code) => { cleanup(); resolve(code === 0); });
107
+ child.on("error", () => { cleanup(); resolve(false); });
108
+ child.stdin.on("error", () => { bail(); });
109
+ child.stdin.write(prompt);
110
+ child.stdin.end();
111
+ } catch (_e) {
112
+ // Catches synchronous throws from spawn (e.g. ENAMETOOLONG on some
113
+ // platforms) or from the initial stdin.write before listeners were
114
+ // attached. Either way: no orphan child, no leaked interval.
115
+ bail();
116
+ }
117
+ });
118
+ }
119
+
120
+ // Run claude -p but CAPTURE stdout instead of inheriting it.
121
+ // Returns the captured stdout string on success, or null on failure.
122
+ // Used for short tasks where we need the response content (e.g. translation).
123
+ // maxBuffer default is 10MB — enough for document translation.
124
+ function runClaudeCapture(prompt, options = {}) {
125
+ try {
126
+ const out = execSync("claude -p --dangerously-skip-permissions", {
127
+ input: prompt,
128
+ cwd: options.cwd || PROJECT_ROOT,
129
+ stdio: ["pipe", "pipe", "pipe"],
130
+ encoding: "utf-8",
131
+ timeout: options.timeout || 0,
132
+ maxBuffer: options.maxBuffer || 10 * 1024 * 1024,
133
+ });
134
+ return out;
135
+ } catch (_e) {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ // ─── Filesystem ─────────────────────────────────────────────────
141
+ // ensureDir: delegated to lib/safe-fs.js (single source of truth)
142
+ // fileExists: alias for existsSafe from lib/safe-fs.js
143
+ const fileExists = existsSafe;
144
+
145
+ // readFile: intentionally throws on error (CLI needs hard failure, unlike safe-fs fallback)
146
+ function readFile(p) {
147
+ return fs.readFileSync(p, "utf-8");
148
+ }
149
+
150
+ function injectProjectRoot(text) {
151
+ // Normalize to forward slashes for prompts (Claude interprets backslashes as escapes)
152
+ const normalizedRoot = PROJECT_ROOT.replace(/\\/g, "/");
153
+ // Use a replacement function so that `$`, `$1`, `$&`, `$$` in the
154
+ // project path (rare but possible on some systems) are preserved as
155
+ // literal characters rather than interpreted as regex specials.
156
+ return text.replace(/\{\{PROJECT_ROOT\}\}/g, () => normalizedRoot);
157
+ }
158
+
159
+ // ─── Helpers ────────────────────────────────────────────────────
160
+ function pad(str, len) {
161
+ return str.length >= len ? str : str + " ".repeat(len - str.length);
162
+ }
163
+
164
+ function countFiles() {
165
+ try {
166
+ let count = 0;
167
+ const skipDirs = ["node_modules", "generated"];
168
+ const visited = new Set();
169
+ const scan = (dir) => {
170
+ if (!fs.existsSync(dir)) return;
171
+ let realDir;
172
+ try { realDir = fs.realpathSync(dir); } catch (_e) { realDir = dir; }
173
+ if (visited.has(realDir)) return;
174
+ visited.add(realDir);
175
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
176
+ if (skipDirs.includes(entry.name)) continue;
177
+ const full = path.join(dir, entry.name);
178
+ if (entry.isDirectory()) scan(full);
179
+ else count++;
180
+ }
181
+ };
182
+ scan(path.join(PROJECT_ROOT, ".claude"));
183
+ scan(path.join(PROJECT_ROOT, "claudeos-core"));
184
+ return count;
185
+ } catch (e) {
186
+ return "?";
187
+ }
188
+ }
189
+
190
+ function countPass1Files() {
191
+ try {
192
+ return fs
193
+ .readdirSync(GENERATED_DIR)
194
+ .filter((f) => f.startsWith("pass1-") && f.endsWith(".json")).length;
195
+ } catch (e) {
196
+ return 0;
197
+ }
198
+ }
199
+
200
+ module.exports = {
201
+ TOOLS_DIR, PROJECT_ROOT, GENERATED_DIR,
202
+ SUPPORTED_LANGS, LANG_CODES, isValidLang,
203
+ log, header, run, runClaudePrompt, runClaudePromptAsync, runClaudeCapture,
204
+ ensureDir, fileExists, readFile, injectProjectRoot,
205
+ pad, countFiles, countPass1Files,
206
+ };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * claude-md-validator — Post-generation structural validation for CLAUDE.md.
3
+ *
4
+ * Runs language-invariant structural checks against a generated CLAUDE.md
5
+ * to detect the §9 re-declaration anti-pattern and other structural drift
6
+ * that the scaffold + prompt-level instructions alone cannot reliably
7
+ * prevent.
8
+ *
9
+ * Usage (as a library):
10
+ * const { validate } = require("./claude-md-validator");
11
+ * const report = validate("/path/to/CLAUDE.md");
12
+ * if (!report.valid) { ... }
13
+ *
14
+ * Usage (as a CLI):
15
+ * node claude-md-validator/index.js /path/to/CLAUDE.md
16
+ * node claude-md-validator/index.js # defaults to ./CLAUDE.md
17
+ *
18
+ * Design principle: every check here must pass/fail identically regardless
19
+ * of the language used to generate CLAUDE.md. See structural-checks.js
20
+ * for the rationale.
21
+ */
22
+
23
+ "use strict";
24
+
25
+ const fs = require("fs");
26
+ const path = require("path");
27
+
28
+ const checks = require("./structural-checks");
29
+ const { formatReport } = require("./reporter");
30
+
31
+ /**
32
+ * Validate a CLAUDE.md file.
33
+ *
34
+ * @param {string} claudeMdPath - Absolute or relative path to CLAUDE.md
35
+ * @returns {object} report with { valid, path, checksRun, errors, warnings, summary }
36
+ */
37
+ function validate(claudeMdPath) {
38
+ // Guard against non-string inputs (null, undefined, numbers, objects).
39
+ // path.resolve() throws TypeError on these, which surfaces as a raw
40
+ // stack trace to CLI users. Returning a structured error keeps the
41
+ // validator's contract simple: every call returns a report object.
42
+ if (typeof claudeMdPath !== "string" || claudeMdPath.length === 0) {
43
+ return {
44
+ valid: false,
45
+ path: String(claudeMdPath),
46
+ checksRun: 0,
47
+ errors: [
48
+ {
49
+ id: "INVALID_PATH",
50
+ pass: false,
51
+ severity: "error",
52
+ message: `Path must be a non-empty string, got ${typeof claudeMdPath}: ${JSON.stringify(claudeMdPath)}`,
53
+ remediation: "Pass a filesystem path to a CLAUDE.md file.",
54
+ },
55
+ ],
56
+ warnings: [],
57
+ summary: "❌ Invalid path argument.",
58
+ };
59
+ }
60
+
61
+ const absPath = path.resolve(claudeMdPath);
62
+
63
+ if (!fs.existsSync(absPath)) {
64
+ return {
65
+ valid: false,
66
+ path: absPath,
67
+ checksRun: 0,
68
+ errors: [
69
+ {
70
+ id: "FILE_MISSING",
71
+ pass: false,
72
+ severity: "error",
73
+ message: `File not found: ${absPath}`,
74
+ remediation:
75
+ "Run `npx claudeos-core init --force` to generate CLAUDE.md.",
76
+ },
77
+ ],
78
+ warnings: [],
79
+ summary: "❌ File not found.",
80
+ };
81
+ }
82
+
83
+ // Guard against the user pointing the validator at a directory or an
84
+ // unreadable file. Without this guard `readFileSync` throws EISDIR /
85
+ // EACCES and the CLI exits with a raw stack trace, which looks like
86
+ // a validator bug to end users.
87
+ const stat = fs.statSync(absPath);
88
+ if (!stat.isFile()) {
89
+ return {
90
+ valid: false,
91
+ path: absPath,
92
+ checksRun: 0,
93
+ errors: [
94
+ {
95
+ id: "NOT_A_FILE",
96
+ pass: false,
97
+ severity: "error",
98
+ message: `Path is not a regular file: ${absPath}`,
99
+ remediation:
100
+ "Point the validator at a CLAUDE.md file, not a directory or special file.",
101
+ },
102
+ ],
103
+ warnings: [],
104
+ summary: "❌ Path is not a file.",
105
+ };
106
+ }
107
+
108
+ let rawContent;
109
+ try {
110
+ rawContent = fs.readFileSync(absPath, "utf8");
111
+ } catch (e) {
112
+ return {
113
+ valid: false,
114
+ path: absPath,
115
+ checksRun: 0,
116
+ errors: [
117
+ {
118
+ id: "FILE_UNREADABLE",
119
+ pass: false,
120
+ severity: "error",
121
+ message: `Could not read file: ${e.message || e}`,
122
+ remediation:
123
+ "Check file permissions and re-run, or regenerate via `npx claudeos-core init --force`.",
124
+ },
125
+ ],
126
+ warnings: [],
127
+ summary: "❌ File unreadable.",
128
+ };
129
+ }
130
+ // Strip UTF-8 BOM (U+FEFF) if present at the start of the file.
131
+ // Some Windows editors and cross-platform generators prepend a BOM to
132
+ // UTF-8 files; without this, the first `## ` line fails to match
133
+ // `^## ` regex checks and the section count is silently off by one.
134
+ const content = rawContent.charCodeAt(0) === 0xfeff ? rawContent.slice(1) : rawContent;
135
+ const sections = checks.splitByH2(content);
136
+
137
+ const results = [];
138
+
139
+ // Structural checks (order does not matter; reporter groups them)
140
+ results.push(checks.checkH2Count(sections));
141
+ results.push(...checks.checkH3Counts(sections));
142
+ results.push(...checks.checkH4Counts(sections));
143
+ results.push(...checks.checkMemoryFileUniqueness(content));
144
+ results.push(...checks.checkMemoryScopedToSection8(sections, content));
145
+ results.push(...checks.checkSectionsHaveContent(sections));
146
+ // Title-invariant check — English canonical token must appear in every
147
+ // `## N.` heading so that multi-repo grep stays consistent across
148
+ // projects generated in different output languages.
149
+ results.push(...checks.checkCanonicalHeadings(sections));
150
+
151
+ const errors = results.filter((r) => !r.pass && r.severity === "error");
152
+ const warnings = results.filter((r) => !r.pass && r.severity === "warning");
153
+ const valid = errors.length === 0;
154
+
155
+ return {
156
+ valid,
157
+ path: absPath,
158
+ checksRun: results.length,
159
+ errors,
160
+ warnings,
161
+ allResults: results,
162
+ summary: valid
163
+ ? `✅ ${results.length} structural checks passed` +
164
+ (warnings.length > 0 ? ` (${warnings.length} warning(s))` : "") +
165
+ "."
166
+ : `❌ ${errors.length} error(s), ${warnings.length} warning(s) out of ${results.length} checks.`,
167
+ };
168
+ }
169
+
170
+ // ─── CLI entry ────────────────────────────────────────────────────
171
+
172
+ if (require.main === module) {
173
+ const target = process.argv[2] || path.join(process.cwd(), "CLAUDE.md");
174
+ const report = validate(target);
175
+ console.log(formatReport(report));
176
+ process.exit(report.valid ? 0 : 1);
177
+ }
178
+
179
+ module.exports = {
180
+ validate,
181
+ // Expose checks for advanced callers / testing
182
+ checks,
183
+ formatReport,
184
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * reporter.js — Human-readable formatter for validator reports.
3
+ *
4
+ * Keeps all user-facing messages in one place so the CLI and the
5
+ * plan-installer integration produce consistent output.
6
+ *
7
+ * No color codes by default — the goal is to work in any terminal
8
+ * without ANSI dependencies and to be easy to grep in CI logs.
9
+ */
10
+
11
+ "use strict";
12
+
13
+ function formatReport(report) {
14
+ const lines = [];
15
+ lines.push("");
16
+ lines.push(` 🔍 Validating: ${report.path}`);
17
+ lines.push("");
18
+ lines.push(` ${report.summary}`);
19
+
20
+ if (report.errors.length > 0) {
21
+ lines.push("");
22
+ lines.push(" Errors:");
23
+ for (const err of report.errors) {
24
+ lines.push(` ❌ [${err.id}] ${err.message || "(no message)"}`);
25
+ if (err.remediation) {
26
+ lines.push(` → ${err.remediation}`);
27
+ }
28
+ }
29
+ }
30
+
31
+ if (report.warnings.length > 0) {
32
+ lines.push("");
33
+ lines.push(" Warnings:");
34
+ for (const warn of report.warnings) {
35
+ lines.push(` ⚠️ [${warn.id}] ${warn.message || "(no message)"}`);
36
+ if (warn.remediation) {
37
+ lines.push(` → ${warn.remediation}`);
38
+ }
39
+ }
40
+ }
41
+
42
+ if (!report.valid) {
43
+ lines.push("");
44
+ lines.push(" To regenerate CLAUDE.md from scratch:");
45
+ lines.push(" npx claudeos-core init --force");
46
+ lines.push("");
47
+ lines.push(" To inspect the scaffold requirements:");
48
+ lines.push(" See pass-prompts/templates/common/claude-md-scaffold.md");
49
+ }
50
+
51
+ lines.push("");
52
+ return lines.join("\n");
53
+ }
54
+
55
+ /**
56
+ * Short one-line summary suitable for inline output during pipeline runs.
57
+ */
58
+ function formatSummaryLine(report) {
59
+ if (report.valid) {
60
+ const warn = report.warnings.length > 0 ? ` (${report.warnings.length} warning(s))` : "";
61
+ return `✅ CLAUDE.md structure valid (${report.checksRun} checks)${warn}`;
62
+ }
63
+ return `⚠️ CLAUDE.md structure: ${report.errors.length} error(s), ${report.warnings.length} warning(s)`;
64
+ }
65
+
66
+ module.exports = { formatReport, formatSummaryLine };