0xray 4.0.1 → 4.0.2

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 (55) hide show
  1. package/.xray/features.json +1 -1
  2. package/AGENTS-consumer.md +4 -4
  3. package/AGENTS.md +3 -3
  4. package/README.md +1 -1
  5. package/SKILLS.md +1 -1
  6. package/dist/AGENTS.md +3 -3
  7. package/dist/CHANGELOG.md +36 -0
  8. package/dist/README.md +1 -1
  9. package/dist/SKILLS.md +1 -1
  10. package/dist/cli/commands/foundry-mint-wear.d.ts +2 -0
  11. package/dist/cli/commands/foundry-mint-wear.js +14 -0
  12. package/dist/cli/commands/hermes-install.js +2 -0
  13. package/dist/cli/commands/openclaw-install.js +2 -0
  14. package/dist/cli/commands/opencode-install.js +2 -0
  15. package/dist/enforcement/enforcer-tools.d.ts +7 -0
  16. package/dist/enforcement/enforcer-tools.js +18 -109
  17. package/dist/integrations/grok/grok-cli.js +2 -0
  18. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  19. package/dist/processors/implementations/version-compliance-processor.d.ts +3 -34
  20. package/dist/processors/implementations/version-compliance-processor.js +8 -135
  21. package/package.json +15 -14
  22. package/scripts/foundry/CHANGELOG.md +34 -0
  23. package/scripts/foundry/LICENSE +21 -0
  24. package/scripts/foundry/README.md +67 -0
  25. package/scripts/foundry/ci-monitor.mjs +115 -0
  26. package/scripts/foundry/cli.js +2 -0
  27. package/scripts/foundry/cli.mjs +55 -0
  28. package/scripts/foundry/docs-build.mjs +38 -0
  29. package/scripts/foundry/hooks.mjs +36 -0
  30. package/scripts/foundry/mill-root.mjs +62 -0
  31. package/scripts/foundry/mint-suit.cjs +313 -0
  32. package/scripts/foundry/mint.mjs +33 -0
  33. package/scripts/foundry/package.json +53 -0
  34. package/scripts/foundry/reconcile-version.mjs +150 -0
  35. package/scripts/foundry/release-gate.mjs +100 -0
  36. package/scripts/foundry/release.mjs +266 -0
  37. package/scripts/foundry/validate-release-docs.mjs +316 -0
  38. package/scripts/foundry/version-manager.mjs +444 -0
  39. package/scripts/node/ci-cd-auto-fix.cjs +12 -257
  40. package/scripts/node/consumer-install-smoke.mjs +2 -0
  41. package/scripts/node/enforce-version-compliance.sh +5 -20
  42. package/scripts/node/enforce-version-compliance.ts +13 -113
  43. package/scripts/node/foundry-shim.mjs +19 -0
  44. package/scripts/node/github-actions-monitor.cjs +13 -18
  45. package/scripts/node/postinstall.cjs +36 -1
  46. package/scripts/node/reconcile-version.mjs +3 -142
  47. package/scripts/node/release-gate.mjs +3 -74
  48. package/scripts/node/release.mjs +3 -171
  49. package/scripts/node/validate-release-docs.mjs +3 -285
  50. package/scripts/node/version-manager.mjs +3 -738
  51. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  52. package/xray/features.json +1 -1
  53. package/scripts/node/release.js +0 -17
  54. package/scripts/node/sync-versions.mjs +0 -18
  55. package/scripts/node/universal-version-manager.js +0 -1043
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * release-gate.mjs — Single release checkpoint.
4
+ *
5
+ * Modes:
6
+ * (default) build + test + consumer smoke (fresh + upgrade merge)
7
+ * --verify-only git + reconcile + release docs + consumer smoke — after push, before publish
8
+ * --skip-smoke skip consumer install smoke
9
+ * --skip-docs skip release docs validation (not recommended for publish)
10
+ *
11
+ * Usage:
12
+ * npx @0xray/foundry gate
13
+ * npx @0xray/foundry gate --verify-only
14
+ */
15
+
16
+ import { execSync } from "child_process";
17
+ import path from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { isXrayExoRepo, millScript, readRootPackage, resolveMillRoot } from "./mill-root.mjs";
20
+
21
+ const rootDir = resolveMillRoot();
22
+ const verifyOnly = process.argv.includes("--verify-only");
23
+ const skipSmoke = process.argv.includes("--skip-smoke");
24
+ const skipDocs = process.argv.includes("--skip-docs");
25
+
26
+ function step(label, cmd) {
27
+ console.log(`\n${"─".repeat(60)}\n🔄 ${label}\n${"─".repeat(60)}\n`);
28
+ execSync(cmd, { cwd: rootDir, stdio: "inherit", env: { ...process.env, FOUNDRY_ROOT: rootDir } });
29
+ }
30
+
31
+ function docsCheckCmd() {
32
+ return `${JSON.stringify(process.execPath)} ${JSON.stringify(millScript("validate-release-docs.mjs"))}`;
33
+ }
34
+
35
+ function runLightGate() {
36
+ const pkg = readRootPackage(rootDir);
37
+ if (verifyOnly) {
38
+ if (!skipDocs) step("1/1 Release docs (light)", docsCheckCmd());
39
+ return;
40
+ }
41
+ if (pkg.scripts.build) step("1/3 Build", "npm run build");
42
+ if (pkg.scripts.test) step("2/3 Tests", "npm test");
43
+ if (!skipDocs) step("3/3 Release docs (light)", docsCheckCmd());
44
+ }
45
+
46
+ function main() {
47
+ const mode = verifyOnly ? "verify" : "full";
48
+ console.log("\n╔════════════════════════════════════════════════════════╗");
49
+ console.log(`║ 🛡️ 0xRay Release Gate (${mode})`.padEnd(57) + "║");
50
+ console.log("╚════════════════════════════════════════════════════════╝");
51
+
52
+ try {
53
+ if (!isXrayExoRepo(rootDir)) {
54
+ runLightGate();
55
+ } else if (verifyOnly) {
56
+ step("1/5 Git + reconcile", "node scripts/node/pre-publish-guard.js --verify-only");
57
+ if (!skipDocs) {
58
+ step("2/5 Release docs", docsCheckCmd());
59
+ }
60
+ step(
61
+ "3/5 Plugin infrastructure",
62
+ "node scripts/test/validate-plugins-e2e.cjs --structural-only",
63
+ );
64
+ if (!skipSmoke) {
65
+ step("4/5 Consumer install smoke", "node scripts/node/consumer-install-smoke.mjs");
66
+ }
67
+ } else {
68
+ step("1/7 Build", "npm run build");
69
+ step("2/7 Tests", "npm test");
70
+ if (!skipDocs) {
71
+ step("3/7 Release docs", docsCheckCmd());
72
+ }
73
+ step(
74
+ "4/7 Consumer hook verifiers",
75
+ "npm run verify:pre-commit-diff && npm run verify:pre-push-diff && node scripts/mjs/verify-delegation-gate-core.mjs --host=grok && node scripts/mjs/verify-delegation-gate-core.mjs --host=hermes && node scripts/mjs/verify-delegation-gate-core.mjs --host=opencode && node scripts/mjs/verify-hermes-session-start.mjs && node scripts/mjs/verify-confer-core.mjs && npm run verify:user-aside && npm run build && node scripts/mjs/verify-pipeline-facets.mjs --package-only",
76
+ );
77
+ step(
78
+ "5/7 Plugin infrastructure",
79
+ "node scripts/test/validate-plugins-e2e.cjs --structural-only",
80
+ );
81
+ if (!skipSmoke) {
82
+ step("6/7 Consumer install smoke (fresh + upgrade)", "node scripts/node/consumer-install-smoke.mjs");
83
+ }
84
+ }
85
+
86
+ console.log("\n╔════════════════════════════════════════════════════════╗");
87
+ console.log("║ ✅ RELEASE GATE PASSED ║");
88
+ console.log("╚════════════════════════════════════════════════════════╝\n");
89
+ } catch {
90
+ console.error("\n❌ RELEASE GATE FAILED — do not tag or publish\n");
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ const isMain =
96
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
97
+
98
+ if (isMain) {
99
+ main();
100
+ }
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Canonical 0xRay release script.
5
+ *
6
+ * 1. Bump version from npm registry baseline (reconcile-version)
7
+ * 2. Release artifacts (CHANGELOG / README / AGENTS / docs) — before gate so docs tests pass
8
+ * 3. Release gate (build + test + consumer smoke)
9
+ * 4. Commit release artifacts → push
10
+ * 5. Verify gate (reconcile + git + release docs + smoke)
11
+ * 6. npm publish (idempotent — skip if version already on registry)
12
+ * 7. Tag → push tag (only after successful publish)
13
+ *
14
+ * Usage:
15
+ * npx @0xray/foundry release [patch|minor|major] --dry-run
16
+ * npx @0xray/foundry release [patch|minor|major] --i-mean-it
17
+ * npx @0xray/foundry release --publish-only --dry-run
18
+ * npx @0xray/foundry release --publish-only --i-mean-it
19
+ *
20
+ * Live bump/commit/push/publish requires --i-mean-it or FOUNDRY_RELEASE=1.
21
+ */
22
+
23
+ import { execSync } from "child_process";
24
+ import fs from "fs";
25
+ import path from "path";
26
+ import { fileURLToPath } from "url";
27
+ import { getReleaseArtifactPaths } from "./version-manager.mjs";
28
+ import {
29
+ isXrayExoRepo,
30
+ millScript,
31
+ readRootPackage,
32
+ resolveMillRoot,
33
+ } from "./mill-root.mjs";
34
+
35
+ const rootDir = resolveMillRoot();
36
+ const args = process.argv.slice(2);
37
+ const dryRun = args.includes("--dry-run");
38
+ const publishOnly = args.includes("--publish-only");
39
+ const releaseType = args.find((a) => ["major", "minor", "patch"].includes(a));
40
+ const optedIn = args.includes("--i-mean-it") || process.env.FOUNDRY_RELEASE === "1";
41
+
42
+ function writeReleaseTrap() {
43
+ process.stderr.write(
44
+ "npx @0xray/foundry release bumps, commits, pushes, and publishes the milled cwd.\n" +
45
+ "Pass --i-mean-it or set FOUNDRY_RELEASE=1. Preview with --dry-run.\n",
46
+ );
47
+ }
48
+
49
+ function run(cmd, label) {
50
+ console.log(`\n> ${cmd}`);
51
+ if (dryRun) {
52
+ console.log(` (dry-run skip: ${label})`);
53
+ return;
54
+ }
55
+ execSync(cmd, {
56
+ cwd: rootDir,
57
+ stdio: "inherit",
58
+ encoding: "utf-8",
59
+ env: { ...process.env, FOUNDRY_ROOT: rootDir },
60
+ });
61
+ }
62
+
63
+ function runMill(script, extraArgs, label) {
64
+ const quoted = [millScript(script), ...extraArgs].map((a) => JSON.stringify(a)).join(" ");
65
+ run(`${JSON.stringify(process.execPath)} ${quoted}`, label);
66
+ }
67
+
68
+ function readVersion() {
69
+ return JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf-8")).version;
70
+ }
71
+
72
+ function packageName() {
73
+ const name = readRootPackage(rootDir).name;
74
+ if (!name) {
75
+ process.stderr.write("package.json name is required to mill publish\n");
76
+ process.exit(1);
77
+ }
78
+ return name;
79
+ }
80
+
81
+ function npmPublishCmd() {
82
+ const pkg = readRootPackage(rootDir);
83
+ if (isXrayExoRepo(rootDir) || pkg.name === "@0xray/foundry") {
84
+ return "npm publish --access public";
85
+ }
86
+ return "npm publish";
87
+ }
88
+
89
+ function currentBranch() {
90
+ return execSync("git rev-parse --abbrev-ref HEAD", { cwd: rootDir, encoding: "utf-8" }).trim();
91
+ }
92
+
93
+ function npmVersionPublished(version) {
94
+ try {
95
+ const out = execSync(`npm view ${packageName()}@${version} version`, {
96
+ cwd: rootDir,
97
+ encoding: "utf-8",
98
+ stdio: ["pipe", "pipe", "pipe"],
99
+ }).trim();
100
+ return out === version;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ function publishIdempotent(version) {
107
+ if (dryRun) {
108
+ console.log(` would: ${npmPublishCmd()} (check ${packageName()}@${version} first)`);
109
+ return;
110
+ }
111
+ if (npmVersionPublished(version)) {
112
+ console.log(`ℹ️ ${packageName()}@${version} already on npm — skipping publish`);
113
+ return;
114
+ }
115
+ execSync(npmPublishCmd(), { cwd: rootDir, stdio: "inherit", encoding: "utf-8" });
116
+ console.log(`✅ Published ${packageName()}@${version}`);
117
+ }
118
+
119
+ async function main() {
120
+ const live = (publishOnly || Boolean(releaseType)) && !dryRun;
121
+ if (live && !optedIn) {
122
+ writeReleaseTrap();
123
+ process.exit(1);
124
+ }
125
+
126
+ if (publishOnly) {
127
+ packageName();
128
+ const version = readVersion();
129
+ runMill("release-gate.mjs", [], "release gate");
130
+ if (isXrayExoRepo(rootDir)) {
131
+ run("npm run prepare-consumer && npm run build", "safe-publish");
132
+ } else if (readRootPackage(rootDir).scripts.build) {
133
+ run("npm run build", "build");
134
+ }
135
+ publishIdempotent(version);
136
+ if (!dryRun) {
137
+ try {
138
+ execSync(`git tag -a v${version} -m "Release v${version}"`, {
139
+ cwd: rootDir,
140
+ stdio: "inherit",
141
+ });
142
+ } catch (e) {
143
+ const msg = String(e.stderr || e.message || "");
144
+ if (!msg.includes("already exists")) throw e;
145
+ }
146
+ }
147
+ run(`git push origin v${version}`, "push tag");
148
+ return;
149
+ }
150
+
151
+ if (!releaseType || !["major", "minor", "patch"].includes(releaseType)) {
152
+ process.stderr.write(
153
+ "Usage: npx @0xray/foundry release [patch|minor|major] [--dry-run|--i-mean-it]\n",
154
+ );
155
+ process.stderr.write(
156
+ " npx @0xray/foundry release --publish-only [--dry-run|--i-mean-it]\n",
157
+ );
158
+ process.exit(1);
159
+ }
160
+
161
+ console.log("╔════════════════════════════════════════════════════════╗");
162
+ console.log("║ 🚀 0xRay Release (canonical) ║");
163
+ console.log("╚════════════════════════════════════════════════════════╝");
164
+ if (dryRun) console.log("\n⚠️ DRY RUN — no writes, commits, tags, or publish\n");
165
+
166
+ // 1. Version bump from npm SSOT
167
+ console.log(`\n📦 Step 1: Bump ${releaseType} from npm baseline`);
168
+ if (dryRun) {
169
+ runMill("reconcile-version.mjs", [releaseType], "version bump (dry)");
170
+ } else {
171
+ runMill("reconcile-version.mjs", [releaseType, "--apply"], "version bump");
172
+ }
173
+ const newVersion = dryRun
174
+ ? execSync(
175
+ `${JSON.stringify(process.execPath)} ${JSON.stringify(millScript("reconcile-version.mjs"))} ${JSON.stringify(releaseType)} --print-target`,
176
+ {
177
+ cwd: rootDir,
178
+ encoding: "utf-8",
179
+ env: { ...process.env, FOUNDRY_ROOT: rootDir },
180
+ },
181
+ ).trim()
182
+ : readVersion();
183
+ console.log(`📌 Release version: ${newVersion}`);
184
+
185
+ // 2. Artifacts BEFORE gate (docs vitest matches package.json version)
186
+ console.log("\n📦 Step 2: Release artifacts (CHANGELOG, README, AGENTS, docs)");
187
+ runMill("version-manager.mjs", ["--artifacts-only"], "release artifacts");
188
+
189
+ // 3. Gate
190
+ console.log("\n📦 Step 3: Release gate (build + test + smoke)");
191
+ runMill("release-gate.mjs", [], "release gate");
192
+
193
+ // 4. Commit + push
194
+ console.log("\n📦 Step 4: Commit & push");
195
+ const releaseFiles = getReleaseArtifactPaths();
196
+ if (!dryRun) {
197
+ run(`git add ${releaseFiles.join(" ")}`, "git add");
198
+ try {
199
+ execSync(`git commit --no-verify -m "release: v${newVersion}"`, { cwd: rootDir, stdio: "inherit" });
200
+ } catch (e) {
201
+ const out = String(e.stdout || e.stderr || "");
202
+ if (!out.includes("nothing to commit")) throw e;
203
+ console.log("ℹ️ Nothing to commit (release files unchanged)");
204
+ }
205
+ } else {
206
+ console.log(` would: git add ${releaseFiles.join(" ")} && git commit -m "release: v${newVersion}"`);
207
+ }
208
+
209
+ const branch = currentBranch();
210
+ run(`git push origin ${branch}`, "git push");
211
+
212
+ // 5. Post-push verify before publish
213
+ console.log("\n📦 Step 5: Verify gate (reconcile + git + docs + smoke)");
214
+ runMill("release-gate.mjs", ["--verify-only"], "verify gate");
215
+
216
+ // 6. Publish (idempotent)
217
+ console.log("\n📦 Step 6: npm publish");
218
+ publishIdempotent(newVersion); // name from package.json via packageName()
219
+
220
+ // 7. Tag after publish
221
+ console.log("\n📦 Step 7: Tag");
222
+ if (!dryRun) {
223
+ try {
224
+ execSync(`git tag -a v${newVersion} -m "Release v${newVersion}"`, { cwd: rootDir, stdio: "inherit" });
225
+ console.log(`✅ Created tag v${newVersion}`);
226
+ } catch (e) {
227
+ const msg = String(e.stderr || e.message || "");
228
+ if (msg.includes("already exists")) {
229
+ console.log(`ℹ️ Tag v${newVersion} already exists`);
230
+ } else {
231
+ throw e;
232
+ }
233
+ }
234
+ } else {
235
+ console.log(` would: git tag -a v${newVersion}`);
236
+ }
237
+ run(`git push origin v${newVersion}`, "push tag");
238
+
239
+ console.log("\n╔════════════════════════════════════════════════════════╗");
240
+ console.log("║ ✅ Release Complete! ║");
241
+ console.log("╚════════════════════════════════════════════════════════╝");
242
+ if (!dryRun) {
243
+ process.stdout.write(`\n📦 ${packageName()}@${newVersion} 🏷 v${newVersion}\n`);
244
+ if (isXrayExoRepo(rootDir)) {
245
+ const tweetPath = path.join(rootDir, "tweets", `v${newVersion}.md`);
246
+ if (!fs.existsSync(tweetPath)) {
247
+ fs.mkdirSync(path.join(rootDir, "tweets"), { recursive: true });
248
+ fs.writeFileSync(
249
+ tweetPath,
250
+ `🎉 0xRay v${newVersion} is LIVE - consumer-safe upgrades!\n...\n\`\`\`\nnpm install 0xray@latest\n\`\`\`\n`,
251
+ );
252
+ process.stdout.write(`📝 Tweet template: tweets/v${newVersion}.md\n`);
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ const isMain =
259
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
260
+
261
+ if (isMain) {
262
+ main().catch((err) => {
263
+ process.stderr.write(`\n❌ Release failed: ${err.message}\n`);
264
+ process.exit(1);
265
+ });
266
+ }
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-release-docs.mjs — Blocking guard for release doc freshness.
4
+ *
5
+ * Ensures README, AGENTS, SKILLS, CHANGELOG, Docusaurus headers, and required
6
+ * guides match package.json version and live framework counts before tag/publish.
7
+ *
8
+ * Usage:
9
+ * npx @0xray/foundry docs-check
10
+ * npx @0xray/foundry docs-check --json
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import {
17
+ buildDocsHeader,
18
+ DOCS_SITE_HEADER_FILES,
19
+ getFrameworkCounts,
20
+ getReleaseArtifactPaths,
21
+ } from './version-manager.mjs';
22
+ import { isXrayExoRepo, resolveMillRoot } from './mill-root.mjs';
23
+
24
+ const STANDARD_HEADER_RE =
25
+ /\*{0,2}\d+\.\d+\*{0,2}\s*—\s*a suit that survives the context window/;
26
+
27
+ /** Patch stamps belong in CHANGELOG, not kernel headers. */
28
+ function kernelLineHasPatchStamp(content) {
29
+ return /^\*\*v?\d+\.\d+\.\d+\*\*/m.test(content);
30
+ }
31
+
32
+ /** Guides that must exist and reference the current release (body or header). */
33
+ const REQUIRED_GUIDES = [
34
+ 'docs-site/docs/guides/features-since-3.1.md',
35
+ 'docs-site/docs/guides/features-json.md',
36
+ 'docs-site/docs/guides/memory-routing.md',
37
+ 'docs-site/docs/guides/aside-context.md',
38
+ 'docs-site/docs/guides/repertoire.md',
39
+ 'docs-site/docs/guides/integrations.md',
40
+ 'docs-site/docs/guides/consumer-migration.md',
41
+ 'docs-site/docs/mcp/README.md',
42
+ 'docs-site/docs/agents/README.md',
43
+ ];
44
+
45
+ const ROOT_DOC_CHECKS = [
46
+ {
47
+ rel: 'README.md',
48
+ validate: (content, _version, _counts, header) => {
49
+ const errors = [];
50
+ if (!content.includes(header) && !STANDARD_HEADER_RE.test(content)) {
51
+ errors.push(`missing kernel header (expected substring: ${header})`);
52
+ }
53
+ if (kernelLineHasPatchStamp(content)) {
54
+ errors.push('kernel header must be era (major.minor), not a patch stamp');
55
+ }
56
+ if (!content.includes('survive the context window') && !content.includes('survives the context window')) {
57
+ errors.push('missing context-window / exo kernel copy');
58
+ }
59
+ if (!content.includes('SKILLS.md')) {
60
+ errors.push('missing SKILLS.md reference in postinstall/docs section');
61
+ }
62
+ if (!content.includes('install-bridges') && !content.includes('installAllBridges')) {
63
+ errors.push('missing install-bridges / installAllBridges reference');
64
+ }
65
+ return errors;
66
+ },
67
+ },
68
+ {
69
+ rel: 'AGENTS.md',
70
+ validate: (content, _version) => {
71
+ const errors = [];
72
+ if (kernelLineHasPatchStamp(content)) {
73
+ errors.push('kernel header must be era (major.minor), not a patch stamp');
74
+ }
75
+ if (!content.includes('survive the context window') && !content.includes('survives the context window') && !content.includes('Exo, not catalog')) {
76
+ errors.push('missing exo / context-window kernel copy');
77
+ }
78
+ if (!content.includes('aside-context.md') && !content.includes('AsideContext')) {
79
+ errors.push('missing AsideContext documentation reference');
80
+ }
81
+ if (!content.includes('SKILLS.md')) {
82
+ errors.push('missing SKILLS.md link');
83
+ }
84
+ return errors;
85
+ },
86
+ },
87
+ {
88
+ rel: 'AGENTS-consumer.md',
89
+ validate: (content) => {
90
+ const errors = [];
91
+ if (kernelLineHasPatchStamp(content)) {
92
+ errors.push('kernel header must be era (major.minor), not a patch stamp');
93
+ }
94
+ if (!content.includes('survive the context window') && !content.includes('survives the context window')) {
95
+ errors.push('consumer header missing context-window kernel copy');
96
+ }
97
+ if (!content.includes('SKILLS.md')) {
98
+ errors.push('missing SKILLS.md in postinstall section');
99
+ }
100
+ if (!content.includes('AsideContext')) {
101
+ errors.push('missing AsideContext section');
102
+ }
103
+ return errors;
104
+ },
105
+ },
106
+ {
107
+ rel: 'SKILLS.md',
108
+ validate: (content, _version, counts) => {
109
+ const errors = [];
110
+ if (kernelLineHasPatchStamp(content)) {
111
+ errors.push('kernel header must be era (major.minor), not a patch stamp');
112
+ }
113
+ if (!content.includes(`**${counts.skills} skills**`)) {
114
+ errors.push(`skill count stale (expected **${counts.skills} skills**)`);
115
+ }
116
+ return errors;
117
+ },
118
+ },
119
+ ];
120
+
121
+ function readFile(rootDir, rel) {
122
+ const full = path.join(rootDir, rel);
123
+ if (!fs.existsSync(full)) return null;
124
+ return fs.readFileSync(full, 'utf-8');
125
+ }
126
+
127
+ function getPackageVersion(rootDir) {
128
+ const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
129
+ return pkg.version;
130
+ }
131
+
132
+ function getCodexTermCount(rootDir) {
133
+ const codexPath = path.join(rootDir, '.xray/codex.json');
134
+ if (!fs.existsSync(codexPath)) return null;
135
+ const codex = JSON.parse(fs.readFileSync(codexPath, 'utf-8'));
136
+ return Object.keys(codex.terms || {}).length;
137
+ }
138
+
139
+ function validateChangelog(content, version) {
140
+ const errors = [];
141
+ const headings = [...content.matchAll(/^## \[([^\]]+)\]/gm)].map((m) => m[1]);
142
+ if (headings.length === 0) {
143
+ errors.push('no version sections found');
144
+ return errors;
145
+ }
146
+ if (headings[0] === 'Unreleased') {
147
+ if (headings[1] !== version) {
148
+ errors.push(`after [Unreleased], expected [${version}], found [${headings[1] || 'none'}]`);
149
+ }
150
+ } else if (headings[0] !== version) {
151
+ errors.push(`top CHANGELOG entry is [${headings[0]}], expected [${version}] or [Unreleased]`);
152
+ }
153
+ if (!content.includes(`## [${version}]`)) {
154
+ errors.push(`missing ## [${version}] section`);
155
+ }
156
+ return errors;
157
+ }
158
+
159
+ function validateStandardHeader(content, expectedHeader, label) {
160
+ if (!content.includes(expectedHeader) && !STANDARD_HEADER_RE.test(content)) {
161
+ return [`${label}: missing kernel header (expected substring: ${expectedHeader})`];
162
+ }
163
+ return [];
164
+ }
165
+
166
+ function validateDocusaurusTagline(content) {
167
+ const errors = [];
168
+ if (!content.includes('survive the context window') && !content.includes('survives the context window')) {
169
+ errors.push('docusaurus.config.ts tagline missing context-window kernel copy');
170
+ }
171
+ return errors;
172
+ }
173
+
174
+ /**
175
+ * @param {string} [rootDir]
176
+ * @returns {{ ok: boolean; version: string; counts: object; errors: string[]; warnings: string[]; mode?: 'full'|'light' }}
177
+ */
178
+ export function validateReleaseDocs(rootDir = resolveMillRoot()) {
179
+ const errors = [];
180
+ const warnings = [];
181
+ const version = getPackageVersion(rootDir);
182
+ const counts = getFrameworkCounts(rootDir);
183
+ const expectedHeader = buildDocsHeader(counts, version);
184
+ const codexTerms = getCodexTermCount(rootDir);
185
+ const full = isXrayExoRepo(rootDir);
186
+
187
+ if (codexTerms !== null && counts.codexTerms && codexTerms !== counts.codexTerms) {
188
+ warnings.push(`codex.json has ${codexTerms} terms (counts.codexTerms is ${counts.codexTerms})`);
189
+ }
190
+
191
+ const changelog = readFile(rootDir, 'CHANGELOG.md');
192
+ if (!changelog) {
193
+ if (full) {
194
+ errors.push('CHANGELOG.md missing');
195
+ } else {
196
+ warnings.push('CHANGELOG.md missing (light mill)');
197
+ }
198
+ } else {
199
+ errors.push(...validateChangelog(changelog, version).map((e) => `CHANGELOG.md: ${e}`));
200
+ }
201
+
202
+ if (!full) {
203
+ return {
204
+ ok: errors.length === 0,
205
+ version,
206
+ counts,
207
+ errors,
208
+ warnings,
209
+ mode: 'light',
210
+ };
211
+ }
212
+
213
+ for (const check of ROOT_DOC_CHECKS) {
214
+ const content = readFile(rootDir, check.rel);
215
+ if (!content) {
216
+ errors.push(`${check.rel}: file missing`);
217
+ continue;
218
+ }
219
+ const relErrors = check.validate(content, version, counts, expectedHeader);
220
+ for (const e of relErrors) {
221
+ errors.push(`${check.rel}: ${e}`);
222
+ }
223
+ }
224
+
225
+ for (const rel of DOCS_SITE_HEADER_FILES) {
226
+ const content = readFile(rootDir, rel);
227
+ if (!content) {
228
+ errors.push(`${rel}: file missing`);
229
+ continue;
230
+ }
231
+ errors.push(...validateStandardHeader(content, expectedHeader, rel));
232
+ if (kernelLineHasPatchStamp(content)) {
233
+ errors.push(`${rel}: kernel header must be era (major.minor), not a patch stamp`);
234
+ }
235
+ }
236
+
237
+ for (const rel of REQUIRED_GUIDES) {
238
+ const content = readFile(rootDir, rel);
239
+ if (!content) {
240
+ errors.push(`${rel}: required guide missing`);
241
+ continue;
242
+ }
243
+ if (content.trim().length < 200) {
244
+ errors.push(`${rel}: guide too short — likely stub`);
245
+ }
246
+
247
+ }
248
+
249
+ const docusaurus = readFile(rootDir, 'docs-site/docusaurus.config.ts');
250
+ if (!docusaurus) {
251
+ errors.push('docs-site/docusaurus.config.ts: missing');
252
+ } else {
253
+ errors.push(...validateDocusaurusTagline(docusaurus));
254
+ }
255
+
256
+ for (const rel of getReleaseArtifactPaths(rootDir)) {
257
+ if (!fs.existsSync(path.join(rootDir, rel))) {
258
+ errors.push(`release artifact path missing: ${rel}`);
259
+ }
260
+ }
261
+
262
+ return {
263
+ ok: errors.length === 0,
264
+ version,
265
+ counts,
266
+ errors,
267
+ warnings,
268
+ mode: 'full',
269
+ };
270
+ }
271
+
272
+ function main() {
273
+ const jsonOut = process.argv.includes('--json');
274
+ const result = validateReleaseDocs();
275
+
276
+ if (!jsonOut) {
277
+ console.log('\n╔════════════════════════════════════════════════════════╗');
278
+ console.log('║ 📚 Release Docs Validation ║');
279
+ console.log('╚════════════════════════════════════════════════════════╝');
280
+ console.log(`\n📌 package.json version: v${result.version}`);
281
+ console.log(
282
+ `📊 counts: ${result.counts.agents} agents · ${result.counts.skills} skills · ${result.counts.mcps} MCP servers\n`,
283
+ );
284
+ }
285
+
286
+ if (result.warnings.length > 0 && !jsonOut) {
287
+ console.log('⚠️ Warnings:');
288
+ for (const w of result.warnings) console.log(` - ${w}`);
289
+ console.log();
290
+ }
291
+
292
+ if (!result.ok) {
293
+ if (jsonOut) {
294
+ console.log(JSON.stringify(result, null, 2));
295
+ } else {
296
+ console.error('❌ Release docs validation FAILED:\n');
297
+ for (const e of result.errors) console.error(` • ${e}`);
298
+ console.error('\nFix docs then run: npm run version:sync');
299
+ console.error('Or: npx @0xray/foundry docs-check\n');
300
+ }
301
+ process.exit(1);
302
+ }
303
+
304
+ if (jsonOut) {
305
+ console.log(JSON.stringify(result, null, 2));
306
+ } else {
307
+ console.log('✅ Release docs validation passed\n');
308
+ }
309
+ }
310
+
311
+ const isMain =
312
+ process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
313
+
314
+ if (isMain) {
315
+ main();
316
+ }