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
@@ -0,0 +1,317 @@
1
+ /**
2
+ * env-parser.js — Parse .env* files for factual project configuration.
3
+ *
4
+ * WHY THIS EXISTS:
5
+ * claudeos-core's "LLMs guess, code confirms" principle requires that
6
+ * factual project data (ports, hosts, API targets) be extracted from
7
+ * declarative sources the project itself maintains, not guessed from
8
+ * framework defaults. `.env.example` is such a source — it's the
9
+ * canonical declaration of a project's runtime configuration surface.
10
+ *
11
+ * Historically, stack-detector only parsed .env for DATABASE_URL to
12
+ * identify the DB. Everything else (ports, hosts, API endpoints) fell
13
+ * back to hardcoded framework defaults (e.g., Vite → 5173), which
14
+ * silently produced wrong values whenever a project customized its
15
+ * configuration via .env. This utility closes that gap.
16
+ *
17
+ * SEARCH ORDER:
18
+ * `.env.example` is preferred over actual `.env` files because it is
19
+ * the shape-of-truth committed to VCS: developer-neutral, reflecting
20
+ * the project's intended configuration surface, not one contributor's
21
+ * local overrides.
22
+ */
23
+
24
+ "use strict";
25
+
26
+ const path = require("path");
27
+ const { readFileSafe, existsSafe } = require("./safe-fs");
28
+
29
+ // Search order: public-facing → developer-specific → runtime-specific.
30
+ // .env.example is canonical because it's the committed, intended config.
31
+ const ENV_FILE_ORDER = [
32
+ ".env.example",
33
+ ".env.local.example",
34
+ ".env.development.example",
35
+ ".env.sample",
36
+ ".env.template",
37
+ ".env",
38
+ ".env.local",
39
+ ".env.development",
40
+ ];
41
+
42
+ // Port variable name conventions across frameworks.
43
+ // Ordered by specificity — more specific wins when multiple are present.
44
+ const PORT_VAR_KEYS = [
45
+ // Vite-specific common patterns
46
+ "VITE_PORT",
47
+ "VITE_DEV_PORT",
48
+ "VITE_DEV_SERVER_PORT",
49
+ "VITE_DESKTOP_PORT",
50
+ // Next.js
51
+ "NEXT_PUBLIC_PORT",
52
+ "NEXT_PORT",
53
+ // Nuxt
54
+ "NUXT_PORT",
55
+ "NUXT_PUBLIC_PORT",
56
+ // Angular
57
+ "NG_PORT",
58
+ "NG_DEV_PORT",
59
+ // Node / backend frameworks
60
+ "APP_PORT",
61
+ "SERVER_PORT",
62
+ "HTTP_PORT",
63
+ "DEV_PORT",
64
+ // Python
65
+ "FLASK_RUN_PORT",
66
+ "UVICORN_PORT",
67
+ "DJANGO_PORT",
68
+ // Generic last — lowest priority because "PORT" collides with too many things
69
+ "PORT",
70
+ ];
71
+
72
+ // Host variable conventions.
73
+ const HOST_VAR_KEYS = [
74
+ "VITE_DEV_HOST",
75
+ "VITE_HOST",
76
+ "NEXT_PUBLIC_HOST",
77
+ "NUXT_HOST",
78
+ "APP_HOST",
79
+ "SERVER_HOST",
80
+ "HTTP_HOST",
81
+ "HOST",
82
+ ];
83
+
84
+ // API target / backend proxy conventions.
85
+ const API_TARGET_VAR_KEYS = [
86
+ "VITE_API_TARGET",
87
+ "VITE_API_URL",
88
+ "VITE_API_BASE_URL",
89
+ "NEXT_PUBLIC_API_URL",
90
+ "NEXT_PUBLIC_API_BASE_URL",
91
+ "NUXT_PUBLIC_API_BASE",
92
+ "API_TARGET",
93
+ "API_URL",
94
+ "API_BASE_URL",
95
+ "BACKEND_URL",
96
+ "PROXY_TARGET",
97
+ ];
98
+
99
+ /**
100
+ * Parse .env-style file content into a flat key-value object.
101
+ * Handles: KEY=VALUE, quoted values, inline comments, blank lines, export prefix.
102
+ * Does NOT expand ${VAR} interpolation — we keep raw declared values.
103
+ */
104
+ function parseEnvContent(content) {
105
+ if (!content || typeof content !== "string") return {};
106
+ const result = {};
107
+ const lines = content.split(/\r?\n/);
108
+ for (const rawLine of lines) {
109
+ let line = rawLine.trim();
110
+ if (!line) continue;
111
+ if (line.startsWith("#")) continue;
112
+ // Strip `export` prefix (common in shell-sourced env files)
113
+ if (line.startsWith("export ")) line = line.slice(7).trim();
114
+ const eq = line.indexOf("=");
115
+ if (eq === -1) continue;
116
+ const key = line.slice(0, eq).trim();
117
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
118
+ let value = line.slice(eq + 1).trim();
119
+ // Strip surrounding single or double quotes
120
+ if (
121
+ (value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
122
+ (value.startsWith("'") && value.endsWith("'") && value.length >= 2)
123
+ ) {
124
+ value = value.slice(1, -1);
125
+ } else {
126
+ // Strip inline comment (only on unquoted values)
127
+ const hashIdx = value.indexOf(" #");
128
+ if (hashIdx !== -1) value = value.slice(0, hashIdx).trim();
129
+ }
130
+ result[key] = value;
131
+ }
132
+ return result;
133
+ }
134
+
135
+ /**
136
+ * Locate the most authoritative env file in a project root.
137
+ * Returns the absolute path, or null if none found.
138
+ */
139
+ function findPrimaryEnvFile(root) {
140
+ for (const name of ENV_FILE_ORDER) {
141
+ const p = path.join(root, name);
142
+ if (existsSafe(p)) return p;
143
+ }
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Read and parse the primary env file. Returns { file, vars } or null.
149
+ */
150
+ function readPrimaryEnv(root) {
151
+ const file = findPrimaryEnvFile(root);
152
+ if (!file) return null;
153
+ const content = readFileSafe(file);
154
+ if (!content) return null;
155
+ return {
156
+ file: path.basename(file),
157
+ vars: parseEnvContent(content),
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Extract a port value from parsed env vars. Returns integer or null.
163
+ * First match by PORT_VAR_KEYS ordering wins.
164
+ */
165
+ function extractPort(vars) {
166
+ if (!vars) return null;
167
+ for (const key of PORT_VAR_KEYS) {
168
+ if (key in vars) {
169
+ const n = parseInt(vars[key], 10);
170
+ if (!Number.isNaN(n) && n > 0 && n < 65536) return n;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * Extract a host value from parsed env vars. Returns string or null.
178
+ */
179
+ function extractHost(vars) {
180
+ if (!vars) return null;
181
+ for (const key of HOST_VAR_KEYS) {
182
+ if (key in vars && vars[key]) return vars[key];
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Extract an API target URL from parsed env vars. Returns string or null.
189
+ */
190
+ function extractApiTarget(vars) {
191
+ if (!vars) return null;
192
+ for (const key of API_TARGET_VAR_KEYS) {
193
+ if (key in vars && vars[key]) return vars[key];
194
+ }
195
+ return null;
196
+ }
197
+
198
+ /**
199
+ * Sensitive variable name patterns. env vars matching any of these patterns
200
+ * are redacted from the `vars` map returned to downstream consumers
201
+ * (stack-detector, prompt-generator, CLAUDE.md scaffold).
202
+ *
203
+ * Even though `.env.example` is conventionally a placeholder file committed
204
+ * to VCS (and should not contain real secrets), projects occasionally check
205
+ * in real values by mistake. claudeos-core piping those values into
206
+ * CLAUDE.md would amplify the leak — CLAUDE.md is committed, shared, and
207
+ * potentially published as part of open-source documentation.
208
+ *
209
+ * Redaction strategy:
210
+ * - Matching keys are kept in the map so consumers can still detect
211
+ * "this variable exists" (e.g., "project declares an API_KEY env var").
212
+ * - Values are replaced with the sentinel string "***REDACTED***".
213
+ * - extractPort / extractHost / extractApiTarget already scan only a
214
+ * whitelist of config-relevant keys (PORT, HOST, API_TARGET, etc.)
215
+ * so sensitive keys cannot leak through those paths regardless of
216
+ * this filter.
217
+ *
218
+ * Patterns are case-insensitive substring matches against the variable name.
219
+ */
220
+ const SENSITIVE_VAR_PATTERNS = [
221
+ /password/i,
222
+ /passwd/i,
223
+ /secret/i,
224
+ /api[_-]?key/i,
225
+ /access[_-]?key/i,
226
+ /private[_-]?key/i,
227
+ /auth[_-]?token/i,
228
+ /token/i, // matches TOKEN, AUTH_TOKEN, GIT_TOKEN, NPM_TOKEN
229
+ // (underscore is a word character in regex \b,
230
+ // so \btoken\b fails to match "_TOKEN" suffix)
231
+ /credential/i,
232
+ /bearer/i,
233
+ /\bsalt\b/i,
234
+ /encryption[_-]?key/i,
235
+ /cert(ificate)?[_-]?key/i,
236
+ /secret[_-]?key/i,
237
+ /client[_-]?secret/i,
238
+ /session[_-]?secret/i,
239
+ /jwt[_-]?secret/i,
240
+ ];
241
+
242
+ /**
243
+ * Returns true if the given env var name matches any sensitive pattern.
244
+ */
245
+ function isSensitiveVarName(name) {
246
+ if (!name || typeof name !== "string") return false;
247
+ return SENSITIVE_VAR_PATTERNS.some(re => re.test(name));
248
+ }
249
+
250
+ /**
251
+ * Redacts sensitive values in an env vars map. Returns a new object;
252
+ * original is not mutated. Preserves keys so "variable exists" signal
253
+ * is kept, but replaces values with a sentinel string.
254
+ *
255
+ * Whitelist exception: DATABASE_URL is kept as-is because stack-detector's
256
+ * db-identification path has always used it and existing project-analysis
257
+ * consumers depend on reading it. (The DB URL contains credentials, but
258
+ * this has been the established behavior since v1.x and changing it would
259
+ * be a breaking change. Downstream consumers that write CLAUDE.md content
260
+ * from vars should still redact it at their layer.)
261
+ */
262
+ function redactSensitiveVars(vars) {
263
+ if (!vars || typeof vars !== "object") return vars;
264
+ const out = {};
265
+ for (const [k, v] of Object.entries(vars)) {
266
+ if (k === "DATABASE_URL") {
267
+ out[k] = v; // documented whitelist for stack-detector back-compat
268
+ } else if (isSensitiveVarName(k)) {
269
+ out[k] = "***REDACTED***";
270
+ } else {
271
+ out[k] = v;
272
+ }
273
+ }
274
+ return out;
275
+ }
276
+
277
+ /**
278
+ * Top-level convenience: read the project's env file and produce the
279
+ * stack.envInfo object consumed by project-analysis.json.
280
+ *
281
+ * Returns null when no env file exists (caller falls back to framework defaults).
282
+ *
283
+ * Sensitive variable values (passwords, secrets, tokens, API keys) are
284
+ * redacted in `vars` via redactSensitiveVars before being returned.
285
+ * extractPort/Host/ApiTarget use a whitelist of config-relevant keys so
286
+ * they are unaffected by redaction.
287
+ */
288
+ function readStackEnvInfo(root) {
289
+ const primary = readPrimaryEnv(root);
290
+ if (!primary) return null;
291
+ const { file, vars } = primary;
292
+ return {
293
+ source: file,
294
+ vars: redactSensitiveVars(vars),
295
+ port: extractPort(vars),
296
+ host: extractHost(vars),
297
+ apiTarget: extractApiTarget(vars),
298
+ };
299
+ }
300
+
301
+ module.exports = {
302
+ parseEnvContent,
303
+ findPrimaryEnvFile,
304
+ readPrimaryEnv,
305
+ extractPort,
306
+ extractHost,
307
+ extractApiTarget,
308
+ readStackEnvInfo,
309
+ isSensitiveVarName,
310
+ redactSensitiveVars,
311
+ // Exported for test visibility:
312
+ ENV_FILE_ORDER,
313
+ PORT_VAR_KEYS,
314
+ HOST_VAR_KEYS,
315
+ API_TARGET_VAR_KEYS,
316
+ SENSITIVE_VAR_PATTERNS,
317
+ };
@@ -1,23 +1,23 @@
1
- /**
2
- * Single source of truth for the 9 guide files Pass 3 must generate.
3
- *
4
- * Used by:
5
- * - bin/commands/init.js Guard 3 (fails Pass 3 if any are missing)
6
- * - content-validator/index.js [5/9] (reports MISSING/EMPTY)
7
- *
8
- * Adding a guide: edit this file only.
9
- */
10
-
11
- const EXPECTED_GUIDE_FILES = [
12
- "01.onboarding/01.overview.md",
13
- "01.onboarding/02.quickstart.md",
14
- "01.onboarding/03.glossary.md",
15
- "02.usage/01.faq.md",
16
- "02.usage/02.real-world-examples.md",
17
- "02.usage/03.do-and-dont.md",
18
- "03.troubleshooting/01.troubleshooting.md",
19
- "04.architecture/01.file-map.md",
20
- "04.architecture/02.pros-and-cons.md",
21
- ];
22
-
23
- module.exports = { EXPECTED_GUIDE_FILES };
1
+ /**
2
+ * Single source of truth for the 9 guide files Pass 3 must generate.
3
+ *
4
+ * Used by:
5
+ * - bin/commands/init.js Guard 3 (fails Pass 3 if any are missing)
6
+ * - content-validator/index.js [5/9] (reports MISSING/EMPTY)
7
+ *
8
+ * Adding a guide: edit this file only.
9
+ */
10
+
11
+ const EXPECTED_GUIDE_FILES = [
12
+ "01.onboarding/01.overview.md",
13
+ "01.onboarding/02.quickstart.md",
14
+ "01.onboarding/03.glossary.md",
15
+ "02.usage/01.faq.md",
16
+ "02.usage/02.real-world-examples.md",
17
+ "02.usage/03.do-and-dont.md",
18
+ "03.troubleshooting/01.troubleshooting.md",
19
+ "04.architecture/01.file-map.md",
20
+ "04.architecture/02.pros-and-cons.md",
21
+ ];
22
+
23
+ module.exports = { EXPECTED_GUIDE_FILES };
@@ -1,90 +1,90 @@
1
- /**
2
- * Pass 3 output directories that must have real content on success.
3
- *
4
- * Complements expected-guides.js (9 guide files). Used by init.js Guard 3
5
- * to detect truncation that occurs AFTER the guide/ section — e.g. Claude
6
- * writes guide/ fully but cuts off before skills/ or plan/.
7
- *
8
- * Severity source: content-validator/index.js. Entries here map 1:1 to
9
- * validator ERROR-level checks. database/ and mcp-guide/ are intentionally
10
- * omitted because the validator flags them as WARNING-level only (stacks
11
- * legitimately skip when no DB or MCP integration is detected).
12
- */
13
-
14
- const fs = require("fs");
15
- const path = require("path");
16
-
17
- const EXPECTED_OUTPUTS = [
18
- {
19
- // Sentinel file written by every stack's pass3.md template.
20
- label: "claudeos-core/standard/00.core/01.project-overview.md",
21
- kind: "file",
22
- relPath: "claudeos-core/standard/00.core/01.project-overview.md",
23
- },
24
- {
25
- // Skills/ holds scaffold-crud-feature (backend) and/or scaffold-page-feature
26
- // (frontend) sub-skills. Zero non-empty .md files = truncation.
27
- label: "claudeos-core/skills/ (≥1 non-empty .md)",
28
- kind: "dir-has-nonempty-md",
29
- relPath: "claudeos-core/skills",
30
- },
31
- // Note: claudeos-core/plan/ check was removed in this version because
32
- // master plan files (10.standard-master.md, 20.rules-master.md, etc.) are
33
- // no longer generated. Master plans were an internal tool backup not
34
- // consumed by Claude Code at runtime, and aggregating many files in a
35
- // single session caused "Prompt is too long" failures on mid-sized
36
- // projects (confirmed on an 18-domain production run).
37
- ];
38
-
39
- function readSafe(p) {
40
- try { return fs.readFileSync(p, "utf-8"); }
41
- catch (_e) { return null; }
42
- }
43
-
44
- // BOM-aware emptiness check. String.prototype.trim does NOT remove U+FEFF
45
- // (not in Unicode White_Space), so a BOM-only file would pass a naive
46
- // `.trim().length === 0` check — we strip it explicitly first. Mirrors
47
- // content-validator/index.js:115 and the H2 check in init.js Guard 3.
48
- function isBlank(text) {
49
- return text.replace(/^\uFEFF/, "").trim().length === 0;
50
- }
51
-
52
- // Stack-based traversal (no recursion limit, same pattern as lib/staged-rules.js
53
- // walkFiles). Returns true on the first non-empty .md found; unreadable dirs
54
- // are skipped silently (matches the project's fault-tolerant fs conventions).
55
- function hasNonEmptyMdRecursive(dir) {
56
- const stack = [dir];
57
- while (stack.length) {
58
- const current = stack.pop();
59
- let entries;
60
- try { entries = fs.readdirSync(current, { withFileTypes: true }); }
61
- catch (_e) { continue; }
62
- for (const e of entries) {
63
- const full = path.join(current, e.name);
64
- if (e.isDirectory()) { stack.push(full); continue; }
65
- if (!e.isFile() || !e.name.endsWith(".md")) continue;
66
- const content = readSafe(full);
67
- if (content !== null && !isBlank(content)) return true;
68
- }
69
- }
70
- return false;
71
- }
72
-
73
- function findMissingOutputs(projectRoot) {
74
- const missing = [];
75
- for (const check of EXPECTED_OUTPUTS) {
76
- const abs = path.join(projectRoot, check.relPath);
77
- if (check.kind === "file") {
78
- if (!fs.existsSync(abs)) { missing.push(`${check.label} — not created`); continue; }
79
- const c = readSafe(abs);
80
- if (c === null) { missing.push(`${check.label} — unreadable`); continue; }
81
- if (isBlank(c)) { missing.push(`${check.label} — empty`); }
82
- } else if (check.kind === "dir-has-nonempty-md") {
83
- if (!fs.existsSync(abs)) { missing.push(`${check.label} — directory missing`); continue; }
84
- if (!hasNonEmptyMdRecursive(abs)) { missing.push(`${check.label} — no non-empty .md files found`); }
85
- }
86
- }
87
- return missing;
88
- }
89
-
90
- module.exports = { EXPECTED_OUTPUTS, findMissingOutputs, hasNonEmptyMdRecursive };
1
+ /**
2
+ * Pass 3 output directories that must have real content on success.
3
+ *
4
+ * Complements expected-guides.js (9 guide files). Used by init.js Guard 3
5
+ * to detect truncation that occurs AFTER the guide/ section — e.g. Claude
6
+ * writes guide/ fully but cuts off before skills/ or plan/.
7
+ *
8
+ * Severity source: content-validator/index.js. Entries here map 1:1 to
9
+ * validator ERROR-level checks. database/ and mcp-guide/ are intentionally
10
+ * omitted because the validator flags them as WARNING-level only (stacks
11
+ * legitimately skip when no DB or MCP integration is detected).
12
+ */
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+
17
+ const EXPECTED_OUTPUTS = [
18
+ {
19
+ // Sentinel file written by every stack's pass3.md template.
20
+ label: "claudeos-core/standard/00.core/01.project-overview.md",
21
+ kind: "file",
22
+ relPath: "claudeos-core/standard/00.core/01.project-overview.md",
23
+ },
24
+ {
25
+ // Skills/ holds scaffold-crud-feature (backend) and/or scaffold-page-feature
26
+ // (frontend) sub-skills. Zero non-empty .md files = truncation.
27
+ label: "claudeos-core/skills/ (≥1 non-empty .md)",
28
+ kind: "dir-has-nonempty-md",
29
+ relPath: "claudeos-core/skills",
30
+ },
31
+ // Note: claudeos-core/plan/ check was removed in this version because
32
+ // master plan files (10.standard-master.md, 20.rules-master.md, etc.) are
33
+ // no longer generated. Master plans were an internal tool backup not
34
+ // consumed by Claude Code at runtime, and aggregating many files in a
35
+ // single session caused "Prompt is too long" failures on mid-sized
36
+ // projects (confirmed on an 18-domain production run).
37
+ ];
38
+
39
+ function readSafe(p) {
40
+ try { return fs.readFileSync(p, "utf-8"); }
41
+ catch (_e) { return null; }
42
+ }
43
+
44
+ // BOM-aware emptiness check. String.prototype.trim does NOT remove U+FEFF
45
+ // (not in Unicode White_Space), so a BOM-only file would pass a naive
46
+ // `.trim().length === 0` check — we strip it explicitly first. Mirrors
47
+ // content-validator/index.js:115 and the H2 check in init.js Guard 3.
48
+ function isBlank(text) {
49
+ return text.replace(/^\uFEFF/, "").trim().length === 0;
50
+ }
51
+
52
+ // Stack-based traversal (no recursion limit, same pattern as lib/staged-rules.js
53
+ // walkFiles). Returns true on the first non-empty .md found; unreadable dirs
54
+ // are skipped silently (matches the project's fault-tolerant fs conventions).
55
+ function hasNonEmptyMdRecursive(dir) {
56
+ const stack = [dir];
57
+ while (stack.length) {
58
+ const current = stack.pop();
59
+ let entries;
60
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); }
61
+ catch (_e) { continue; }
62
+ for (const e of entries) {
63
+ const full = path.join(current, e.name);
64
+ if (e.isDirectory()) { stack.push(full); continue; }
65
+ if (!e.isFile() || !e.name.endsWith(".md")) continue;
66
+ const content = readSafe(full);
67
+ if (content !== null && !isBlank(content)) return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ function findMissingOutputs(projectRoot) {
74
+ const missing = [];
75
+ for (const check of EXPECTED_OUTPUTS) {
76
+ const abs = path.join(projectRoot, check.relPath);
77
+ if (check.kind === "file") {
78
+ if (!fs.existsSync(abs)) { missing.push(`${check.label} — not created`); continue; }
79
+ const c = readSafe(abs);
80
+ if (c === null) { missing.push(`${check.label} — unreadable`); continue; }
81
+ if (isBlank(c)) { missing.push(`${check.label} — empty`); }
82
+ } else if (check.kind === "dir-has-nonempty-md") {
83
+ if (!fs.existsSync(abs)) { missing.push(`${check.label} — directory missing`); continue; }
84
+ if (!hasNonEmptyMdRecursive(abs)) { missing.push(`${check.label} — no non-empty .md files found`); }
85
+ }
86
+ }
87
+ return missing;
88
+ }
89
+
90
+ module.exports = { EXPECTED_OUTPUTS, findMissingOutputs, hasNonEmptyMdRecursive };
@@ -1,35 +1,35 @@
1
- /**
2
- * ClaudeOS-Core — Language Configuration (single source of truth)
3
- *
4
- * The 10-language map for generated documentation output. Used by:
5
- * - bin/lib/cli-utils.js (re-exports as SUPPORTED_LANGS / LANG_CODES / isValidLang)
6
- * - bin/lib/lang-selector.js (interactive picker)
7
- * - lib/memory-scaffold.js (translation prompt — re-exports as LANG_LABELS)
8
- *
9
- * Adding a language: add the entry here. All consumers pick it up automatically.
10
- *
11
- * Format: lang code → human-readable name (native script + English in parens
12
- * for non-English languages). Order is preserved by Object.keys, which drives
13
- * the lang-selector display order — `en` first, then alphabetical-ish.
14
- */
15
-
16
- const LANGUAGES = {
17
- en: "English",
18
- ko: "한국어 (Korean)",
19
- "zh-CN": "简体中文 (Chinese Simplified)",
20
- ja: "日本語 (Japanese)",
21
- es: "Español (Spanish)",
22
- vi: "Tiếng Việt (Vietnamese)",
23
- hi: "हिन्दी (Hindi)",
24
- ru: "Русский (Russian)",
25
- fr: "Français (French)",
26
- de: "Deutsch (German)",
27
- };
28
-
29
- const LANG_CODES = Object.keys(LANGUAGES);
30
-
31
- function isValidLang(lang) {
32
- return LANG_CODES.includes(lang);
33
- }
34
-
35
- module.exports = { LANGUAGES, LANG_CODES, isValidLang };
1
+ /**
2
+ * ClaudeOS-Core — Language Configuration (single source of truth)
3
+ *
4
+ * The 10-language map for generated documentation output. Used by:
5
+ * - bin/lib/cli-utils.js (re-exports as SUPPORTED_LANGS / LANG_CODES / isValidLang)
6
+ * - bin/lib/lang-selector.js (interactive picker)
7
+ * - lib/memory-scaffold.js (translation prompt — re-exports as LANG_LABELS)
8
+ *
9
+ * Adding a language: add the entry here. All consumers pick it up automatically.
10
+ *
11
+ * Format: lang code → human-readable name (native script + English in parens
12
+ * for non-English languages). Order is preserved by Object.keys, which drives
13
+ * the lang-selector display order — `en` first, then alphabetical-ish.
14
+ */
15
+
16
+ const LANGUAGES = {
17
+ en: "English",
18
+ ko: "한국어 (Korean)",
19
+ "zh-CN": "简体中文 (Chinese Simplified)",
20
+ ja: "日本語 (Japanese)",
21
+ es: "Español (Spanish)",
22
+ vi: "Tiếng Việt (Vietnamese)",
23
+ hi: "हिन्दी (Hindi)",
24
+ ru: "Русский (Russian)",
25
+ fr: "Français (French)",
26
+ de: "Deutsch (German)",
27
+ };
28
+
29
+ const LANG_CODES = Object.keys(LANGUAGES);
30
+
31
+ function isValidLang(lang) {
32
+ return LANG_CODES.includes(lang);
33
+ }
34
+
35
+ module.exports = { LANGUAGES, LANG_CODES, isValidLang };