@lifeaitools/rdc-skills 0.24.41 → 0.25.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 (198) hide show
  1. package/.claude/settings.json +15 -15
  2. package/.claude-plugin/marketplace.json +21 -21
  3. package/.claude-plugin/plugin.json +1518 -1371
  4. package/.github/workflows/publish.yml +34 -34
  5. package/.github/workflows/self-test.yml +58 -58
  6. package/CHANGELOG.md +310 -310
  7. package/LICENSE +21 -21
  8. package/MANIFEST.md +221 -221
  9. package/README.md +375 -376
  10. package/README.sandbox.md +3 -3
  11. package/assets/watcher/viewer.html +164 -164
  12. package/bin/rdc-skills-mcp.mjs +316 -316
  13. package/commands/build.md +183 -183
  14. package/commands/collab.md +180 -180
  15. package/commands/deploy.md +152 -152
  16. package/commands/design.md +31 -31
  17. package/commands/edit.md +28 -28
  18. package/commands/fixit.md +150 -124
  19. package/commands/handoff.md +173 -173
  20. package/commands/help.md +95 -95
  21. package/commands/overnight.md +220 -220
  22. package/commands/plan.md +158 -158
  23. package/commands/preplan.md +131 -131
  24. package/commands/prototype.md +145 -145
  25. package/commands/release.md +49 -49
  26. package/commands/report.md +99 -99
  27. package/commands/review.md +120 -120
  28. package/commands/self-test.md +113 -113
  29. package/commands/status.md +86 -86
  30. package/commands/watch.md +98 -98
  31. package/commands/workitems.md +137 -137
  32. package/git-sha.json +1 -1
  33. package/guides/agent-bootstrap.md +295 -295
  34. package/guides/agents/backend.md +104 -104
  35. package/guides/agents/content.md +94 -94
  36. package/guides/agents/cs2.md +56 -56
  37. package/guides/agents/data.md +87 -87
  38. package/guides/agents/design.md +77 -77
  39. package/guides/agents/frontend.md +92 -92
  40. package/guides/agents/infrastructure.md +81 -81
  41. package/guides/agents/setup.md +281 -281
  42. package/guides/agents/verify.md +151 -151
  43. package/guides/agents/viz.md +106 -106
  44. package/guides/backend.md +146 -146
  45. package/guides/content.md +147 -147
  46. package/guides/cs2.md +190 -190
  47. package/guides/data.md +123 -123
  48. package/guides/design.md +116 -116
  49. package/guides/engineering-behavior.md +43 -43
  50. package/guides/escalation-protocol.md +125 -125
  51. package/guides/frontend.md +151 -151
  52. package/guides/history-md-spec.md +297 -297
  53. package/guides/infrastructure.md +179 -179
  54. package/guides/lessons-learned-spec.md +145 -151
  55. package/guides/output-contract.md +108 -108
  56. package/guides/publish-md-spec.md +289 -289
  57. package/guides/rdc-skills-startup.md +30 -30
  58. package/guides/verify.md +11 -11
  59. package/hooks/check-cwd.js +31 -31
  60. package/hooks/check-rdc-environment.js +164 -164
  61. package/hooks/check-services.js +6 -6
  62. package/hooks/check-stale-work-items.js +19 -19
  63. package/hooks/foreground-process-gate.js +128 -128
  64. package/hooks/gate-watchdog-selfcheck.js +257 -257
  65. package/hooks/hook-logger.js +25 -25
  66. package/hooks/lib/run-evidence-gate.mjs +241 -241
  67. package/hooks/no-stop-open-epics.js +127 -127
  68. package/hooks/post-tool-batch-gate.js +203 -203
  69. package/hooks/post-work-check.js +21 -21
  70. package/hooks/postcompact-log.js +13 -13
  71. package/hooks/precompact-log.js +13 -13
  72. package/hooks/rate-limit-retry.js +46 -46
  73. package/hooks/rdc-invocation-marker.js +157 -157
  74. package/hooks/rdc-output-contract-gate.js +94 -94
  75. package/hooks/require-work-item-on-commit.js +294 -294
  76. package/hooks/restart-brief.js +19 -19
  77. package/hooks/run-hidden-hook.ps1 +47 -47
  78. package/hooks/task-completed-gate.js +274 -274
  79. package/hooks/work-item-exit-gate.js +944 -944
  80. package/lib/catalog.mjs +236 -236
  81. package/lib/cloud-rewrite.mjs +155 -155
  82. package/package.json +57 -57
  83. package/rules/work-items-rpc.md +520 -520
  84. package/scaffold/templates/HISTORY.md.template +39 -39
  85. package/scaffold/templates/PUBLISH.md.template +21 -21
  86. package/scaffold/templates/brochure-studio-default.html +70 -70
  87. package/scripts/acceptance.mjs +502 -502
  88. package/scripts/fixtures/guides/bad-guide.md +15 -15
  89. package/scripts/fixtures/guides-clean/good-guide.md +16 -16
  90. package/scripts/install-rdc-skills.js +1289 -1289
  91. package/scripts/install.ps1 +202 -202
  92. package/scripts/install.sh +132 -132
  93. package/scripts/lib/assertions.mjs +287 -287
  94. package/scripts/lib/manifest-schema.mjs +754 -754
  95. package/scripts/lib/runner.mjs +465 -465
  96. package/scripts/lib/sandbox.mjs +435 -435
  97. package/scripts/prepack.mjs +32 -32
  98. package/scripts/rdc-brochure.mjs +482 -482
  99. package/scripts/rdc-design-cli.mjs +134 -134
  100. package/scripts/rebuild-mcp.mjs +107 -107
  101. package/scripts/self-test.mjs +1460 -1460
  102. package/scripts/stamp-git-sha.mjs +29 -29
  103. package/scripts/test-guide-validator.mjs +196 -196
  104. package/scripts/test-rdc-hooks.mjs +145 -145
  105. package/scripts/uninstall.ps1 +77 -77
  106. package/scripts/uninstall.sh +69 -69
  107. package/scripts/update.ps1 +43 -43
  108. package/scripts/update.sh +43 -43
  109. package/scripts/validate-place-histories.js +461 -461
  110. package/scripts/validate-publish-manifests.js +502 -424
  111. package/scripts/watch-init.mjs +100 -100
  112. package/skills/brochure/SKILL.md +107 -107
  113. package/skills/build/SKILL.md +563 -563
  114. package/skills/channel-formatter/SKILL.md +538 -533
  115. package/skills/co-develop/SKILL.md +196 -196
  116. package/skills/collab/SKILL.md +239 -239
  117. package/skills/convert/SKILL.md +167 -140
  118. package/skills/deploy/SKILL.md +541 -541
  119. package/skills/design/SKILL.md +211 -211
  120. package/skills/design/reference/ownership.md +16 -16
  121. package/skills/design/reference/rampa.md +92 -92
  122. package/skills/design/reference/studio-model.md +153 -153
  123. package/skills/edit/SKILL.md +98 -98
  124. package/skills/fixit/SKILL.md +203 -165
  125. package/skills/fs-mcp/SKILL.md +148 -148
  126. package/skills/handoff/SKILL.md +236 -236
  127. package/skills/help/SKILL.md +143 -143
  128. package/skills/housekeeping/SKILL.md +160 -219
  129. package/skills/lifeai-brochure-author/SKILL.md +340 -340
  130. package/skills/onramp/SKILL.md +248 -0
  131. package/skills/overnight/SKILL.md +251 -251
  132. package/skills/plan/SKILL.md +345 -345
  133. package/skills/preplan/SKILL.md +90 -90
  134. package/skills/prototype/SKILL.md +150 -150
  135. package/skills/rdc-brochurify/SKILL.md +245 -245
  136. package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
  137. package/skills/release/SKILL.md +140 -140
  138. package/skills/report/SKILL.md +100 -100
  139. package/skills/review/SKILL.md +152 -152
  140. package/skills/rpms-filemap/SKILL.cloud.md +111 -111
  141. package/skills/rpms-filemap/SKILL.md +111 -111
  142. package/skills/self-test/SKILL.md +132 -132
  143. package/skills/status/SKILL.md +99 -99
  144. package/skills/terminal-config/SKILL.md +62 -62
  145. package/skills/tests/MATRIX.md +54 -54
  146. package/skills/tests/README.md +47 -47
  147. package/skills/tests/onramp.test.json +87 -0
  148. package/skills/tests/rdc-brochure.test.json +34 -34
  149. package/skills/tests/rdc-build.test.json +36 -36
  150. package/skills/tests/rdc-channel-formatter.test.json +45 -45
  151. package/skills/tests/rdc-co-develop.test.json +29 -29
  152. package/skills/tests/rdc-collab.test.json +29 -29
  153. package/skills/tests/rdc-convert.test.json +35 -35
  154. package/skills/tests/rdc-deploy.test.json +30 -30
  155. package/skills/tests/rdc-design.test.json +27 -27
  156. package/skills/tests/rdc-edit.test.json +29 -29
  157. package/skills/tests/rdc-fixit.test.json +36 -36
  158. package/skills/tests/rdc-fs-mcp.test.json +36 -36
  159. package/skills/tests/rdc-handoff.test.json +28 -28
  160. package/skills/tests/rdc-help.test.json +29 -29
  161. package/skills/tests/rdc-housekeeping.test.json +28 -32
  162. package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
  163. package/skills/tests/rdc-overnight.test.json +37 -37
  164. package/skills/tests/rdc-plan.test.json +27 -27
  165. package/skills/tests/rdc-preplan.test.json +31 -31
  166. package/skills/tests/rdc-prototype.test.json +28 -28
  167. package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
  168. package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
  169. package/skills/tests/rdc-release.test.json +29 -29
  170. package/skills/tests/rdc-report.test.json +28 -28
  171. package/skills/tests/rdc-review.test.json +29 -29
  172. package/skills/tests/rdc-rpms-filemap.test.json +28 -28
  173. package/skills/tests/rdc-self-test.test.json +24 -24
  174. package/skills/tests/rdc-status.test.json +29 -29
  175. package/skills/tests/rdc-terminal-config.test.json +29 -29
  176. package/skills/tests/rdc-watch.test.json +24 -24
  177. package/skills/tests/rdc-workitems.test.json +27 -27
  178. package/skills/watch/SKILL.md +97 -97
  179. package/skills/workitems/SKILL.md +151 -151
  180. package/tests/acceptance.test.mjs +59 -59
  181. package/tests/channel-formatter.contract.test.mjs +251 -251
  182. package/tests/curl-surface.test.mjs +289 -289
  183. package/tests/harness-gates.test.mjs +325 -325
  184. package/tests/help-surface.test.mjs +61 -61
  185. package/tests/install-rdc-skills.test.mjs +49 -49
  186. package/tests/manifest-contract-fields.test.mjs +78 -78
  187. package/tests/mcp.test.mjs +271 -271
  188. package/tests/rdc-brochure.test.mjs +125 -125
  189. package/tests/require-work-item-on-commit.test.mjs +162 -162
  190. package/tests/run-evidence-gate.test.mjs +82 -82
  191. package/tests/skill-test-matrix.test.mjs +66 -66
  192. package/tests/validate-skills.js +27 -27
  193. package/tests/work-item-exit-gate-l2.test.mjs +368 -368
  194. package/tests/work-item-exit-gate-l3.test.mjs +197 -197
  195. package/RELEASE.md +0 -42
  196. package/tests/housekeeping-lessons-triage.test.mjs +0 -49
  197. package/tests/lessons-pipeline-contract.test.mjs +0 -26
  198. package/tests/release-contract.test.mjs +0 -16
@@ -1,29 +1,29 @@
1
- #!/usr/bin/env node
2
- /**
3
- * stamp-git-sha.mjs — bake the current git HEAD into git-sha.json at pack/publish
4
- * time. The MCP server runs from the npm-installed copy (no .git), so it cannot
5
- * `git rev-parse` at runtime; this build-time stamp is how /health reports a
6
- * PROVABLE commit ("running artifact == this exact published commit"). Run from
7
- * `prepack`, so `npm pack`/`npm publish` (incl. the publish.yml CI job at the
8
- * tagged commit) embeds the right SHA. Never fails the build — writes 'unknown'
9
- * if git is unavailable (e.g. a tarball-only checkout).
10
- */
11
- import { execFileSync } from 'node:child_process';
12
- import { writeFileSync } from 'node:fs';
13
- import path from 'node:path';
14
- import { fileURLToPath } from 'node:url';
15
-
16
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
- let sha = 'unknown';
18
- try {
19
- sha = execFileSync('git', ['rev-parse', 'HEAD'], {
20
- cwd: root,
21
- encoding: 'utf8',
22
- stdio: ['ignore', 'pipe', 'ignore'],
23
- }).trim();
24
- } catch {
25
- sha = process.env.RDC_SKILLS_GIT_SHA || 'unknown';
26
- }
27
- const out = path.join(root, 'git-sha.json');
28
- writeFileSync(out, JSON.stringify({ sha }, null, 2) + '\n');
29
- console.error(`[stamp-git-sha] wrote ${out} sha=${sha}`);
1
+ #!/usr/bin/env node
2
+ /**
3
+ * stamp-git-sha.mjs — bake the current git HEAD into git-sha.json at pack/publish
4
+ * time. The MCP server runs from the npm-installed copy (no .git), so it cannot
5
+ * `git rev-parse` at runtime; this build-time stamp is how /health reports a
6
+ * PROVABLE commit ("running artifact == this exact published commit"). Run from
7
+ * `prepack`, so `npm pack`/`npm publish` (incl. the publish.yml CI job at the
8
+ * tagged commit) embeds the right SHA. Never fails the build — writes 'unknown'
9
+ * if git is unavailable (e.g. a tarball-only checkout).
10
+ */
11
+ import { execFileSync } from 'node:child_process';
12
+ import { writeFileSync } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ let sha = 'unknown';
18
+ try {
19
+ sha = execFileSync('git', ['rev-parse', 'HEAD'], {
20
+ cwd: root,
21
+ encoding: 'utf8',
22
+ stdio: ['ignore', 'pipe', 'ignore'],
23
+ }).trim();
24
+ } catch {
25
+ sha = process.env.RDC_SKILLS_GIT_SHA || 'unknown';
26
+ }
27
+ const out = path.join(root, 'git-sha.json');
28
+ writeFileSync(out, JSON.stringify({ sha }, null, 2) + '\n');
29
+ console.error(`[stamp-git-sha] wrote ${out} sha=${sha}`);
@@ -1,196 +1,196 @@
1
- #!/usr/bin/env node
2
- // test-guide-validator.mjs — Unit test for the guide-content validator
3
- //
4
- // Proves:
5
- // 1. The validator flags guides containing banned terms in positive-instruction context
6
- // 2. The validator passes guides that mention banned terms only as warnings/negations
7
- // 3. The validator flags unknown clauth key names
8
- // 4. The validator passes known clauth key names
9
- //
10
- // Usage:
11
- // node scripts/test-guide-validator.mjs
12
- //
13
- // Exit codes:
14
- // 0 = all assertions pass
15
- // 1 = one or more assertions failed
16
-
17
- import { dirname, join, resolve } from "node:path";
18
- import { fileURLToPath } from "node:url";
19
-
20
- const __dirname = dirname(fileURLToPath(import.meta.url));
21
- const REPO_ROOT = resolve(__dirname, "..");
22
-
23
- // Import the validator functions from self-test.mjs by running it in test mode.
24
- // Since self-test.mjs is not a module that exports cleanly, we replicate the
25
- // validator logic here using the same constants and algorithms.
26
- // (The shared implementation lives in runGuideContentValidator / auditGuideFile.)
27
- //
28
- // We import via dynamic import of the self-test module's exported helpers —
29
- // but since self-test.mjs has no exports (it runs immediately), we inline
30
- // the equivalent test using the same fixture files it targets.
31
-
32
- // ─── Inline validator (mirrors self-test.mjs logic) ────────────────────────
33
- import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
34
-
35
- const GUIDE_BANNED_TERMS = [
36
- "@masonator/coolify-mcp",
37
- "@masonator",
38
- "coolify-mcp",
39
- "@regen/brand-studio",
40
- "brand-studio",
41
- ];
42
-
43
- const GUIDE_NEGATION_PATTERNS = [
44
- /\bdo not\b/i,
45
- /\bnever\b/i,
46
- /\bno such\b/i,
47
- /\bdoes not exist\b/i,
48
- /\bbanned\b/i,
49
- /\bnot reference\b/i,
50
- /\bnot use\b/i,
51
- /\bavoid\b/i,
52
- /\bremoved\b/i,
53
- /\bdeprecated\b/i,
54
- // Markdown table row showing a WRONG→CORRECT mapping (naming-corrections.md pattern)
55
- /^\|[^|]*WRONG[^|]*\|/i,
56
- // A table row where the term is in the WRONG column (first data column after the | WRONG | header)
57
- /^\|\s*(Brand Studio|brand-studio|@regen\/brand-studio|@masonator[^ |]*|coolify-mcp)[^|]*\|\s*\*\*/,
58
- ];
59
-
60
- const KNOWN_CLAUTH_KEYS = new Set([
61
- "coolify-api",
62
- "cloudflare",
63
- "npm",
64
- "supabase",
65
- "supabase-anon",
66
- "supabase-db",
67
- "supabase-service",
68
- "supabase-service-role",
69
- "r2-access-key-id",
70
- "r2-secret-key",
71
- "anthropic",
72
- "openai",
73
- "github",
74
- "github-token",
75
- "vultr-dev-ssh",
76
- "mcp-web-research-secret",
77
- "mcp-regen-media-secret",
78
- "web-research",
79
- "regen-media",
80
- ]);
81
-
82
- function auditGuideFile(filepath, relPath) {
83
- const findings = [];
84
- let text;
85
- try {
86
- text = readFileSync(filepath, "utf8");
87
- } catch (e) {
88
- findings.push({ level: "error", code: "guide-unreadable", file: relPath, line: 0, message: `cannot read: ${e.message}` });
89
- return findings;
90
- }
91
- const lines = text.split(/\r?\n/);
92
- lines.forEach((line, i) => {
93
- const lineNo = i + 1;
94
- for (const term of GUIDE_BANNED_TERMS) {
95
- if (line.includes(term)) {
96
- const isNegated = GUIDE_NEGATION_PATTERNS.some((re) => re.test(line));
97
- if (!isNegated) {
98
- findings.push({ level: "error", code: "guide-banned-term", file: relPath, line: lineNo, message: `banned term "${term}"` });
99
- }
100
- }
101
- }
102
- const clauthKeyRe = /\bhttp:\/\/127\.0\.0\.1:52437\/v\/([\w-]+)/g;
103
- let m;
104
- while ((m = clauthKeyRe.exec(line)) !== null) {
105
- const key = m[1];
106
- if (!KNOWN_CLAUTH_KEYS.has(key)) {
107
- findings.push({ level: "warn", code: "guide-clauth-key-unknown", file: relPath, line: lineNo, message: `unknown clauth key "${key}"` });
108
- }
109
- }
110
- });
111
- return findings;
112
- }
113
-
114
- function scanDir(dir, prefix) {
115
- const findings = [];
116
- if (!existsSync(dir)) return findings;
117
- let entries;
118
- try { entries = readdirSync(dir); } catch { return findings; }
119
- for (const entry of entries) {
120
- const fullPath = join(dir, entry);
121
- let stat;
122
- try { stat = statSync(fullPath); } catch { continue; }
123
- if (stat.isDirectory()) {
124
- findings.push(...scanDir(fullPath, `${prefix}/${entry}`));
125
- } else if (entry.endsWith(".md")) {
126
- findings.push(...auditGuideFile(fullPath, `${prefix}/${entry}`));
127
- }
128
- }
129
- return findings;
130
- }
131
-
132
- // ─── Test runner ─────────────────────────────────────────────────────────────
133
-
134
- let passed = 0;
135
- let failed = 0;
136
-
137
- function assert(description, condition, detail = "") {
138
- if (condition) {
139
- console.log(` ✓ ${description}`);
140
- passed++;
141
- } else {
142
- console.error(` ✗ FAIL: ${description}${detail ? ` — ${detail}` : ""}`);
143
- failed++;
144
- }
145
- }
146
-
147
- console.log("\nrdc:self-test — guide-content validator unit tests\n");
148
-
149
- // ─── Test 1: bad-guide.md should produce errors for banned terms ─────────────
150
- console.log("Test 1: bad-guide.md flags banned terms");
151
- const BAD_FIXTURE = join(REPO_ROOT, "scripts/fixtures/guides/bad-guide.md");
152
- const badFindings = auditGuideFile(BAD_FIXTURE, "fixtures/guides/bad-guide.md");
153
- const badErrors = badFindings.filter((f) => f.level === "error" && f.code === "guide-banned-term");
154
- assert("bad-guide.md produces at least 2 banned-term errors", badErrors.length >= 2, `got ${badErrors.length}`);
155
- assert("flags @masonator/coolify-mcp", badErrors.some((f) => f.message.includes("@masonator/coolify-mcp")));
156
- assert("flags brand-studio or @regen/brand-studio", badErrors.some((f) => f.message.includes("brand-studio")));
157
-
158
- // ─── Test 2: bad-guide.md flags unknown clauth key ───────────────────────────
159
- console.log("\nTest 2: bad-guide.md flags unknown clauth key");
160
- const badKeyWarnings = badFindings.filter((f) => f.code === "guide-clauth-key-unknown");
161
- assert("bad-guide.md warns on unknown clauth key 'nonexistent-key'", badKeyWarnings.length >= 1, `got ${badKeyWarnings.length}`);
162
- assert("warning message references nonexistent-key", badKeyWarnings.some((f) => f.message.includes("nonexistent-key")));
163
-
164
- // ─── Test 3: good-guide.md produces no errors (negated mentions are OK) ──────
165
- console.log("\nTest 3: good-guide.md passes (negated mentions are not flagged)");
166
- const GOOD_FIXTURE = join(REPO_ROOT, "scripts/fixtures/guides-clean/good-guide.md");
167
- const goodFindings = auditGuideFile(GOOD_FIXTURE, "fixtures/guides-clean/good-guide.md");
168
- const goodErrors = goodFindings.filter((f) => f.level === "error");
169
- assert("good-guide.md produces 0 banned-term errors", goodErrors.length === 0, `got ${goodErrors.length}: ${goodErrors.map((f) => f.message).join("; ")}`);
170
-
171
- // good-guide uses coolify-api which is a known key — no key warnings
172
- const goodKeyWarnings = goodFindings.filter((f) => f.code === "guide-clauth-key-unknown");
173
- assert("good-guide.md produces 0 unknown-key warnings", goodKeyWarnings.length === 0, `got ${goodKeyWarnings.length}`);
174
-
175
- // ─── Test 4: scan fixture directory returns combined findings ─────────────────
176
- console.log("\nTest 4: scanDir finds findings across fixture dirs");
177
- const BAD_DIR = join(REPO_ROOT, "scripts/fixtures/guides");
178
- const dirFindings = scanDir(BAD_DIR, "fixtures");
179
- const dirErrors = dirFindings.filter((f) => f.level === "error");
180
- assert("scanDir on bad fixture dir returns errors", dirErrors.length >= 2, `got ${dirErrors.length}`);
181
-
182
- const GOOD_DIR = join(REPO_ROOT, "scripts/fixtures/guides-clean");
183
- const cleanFindings = scanDir(GOOD_DIR, "fixtures");
184
- const cleanErrors = cleanFindings.filter((f) => f.level === "error");
185
- assert("scanDir on clean fixture dir returns 0 errors", cleanErrors.length === 0, `got ${cleanErrors.length}`);
186
-
187
- // ─── Summary ─────────────────────────────────────────────────────────────────
188
- console.log(`\n${"─".repeat(60)}`);
189
- console.log(`guide-validator tests: ${passed} passed, ${failed} failed`);
190
- if (failed > 0) {
191
- console.error(`\n❌ FAIL — ${failed} assertion(s) did not pass`);
192
- process.exit(1);
193
- } else {
194
- console.log(`\n✓ PASS — all guide-content validator assertions passed`);
195
- process.exit(0);
196
- }
1
+ #!/usr/bin/env node
2
+ // test-guide-validator.mjs — Unit test for the guide-content validator
3
+ //
4
+ // Proves:
5
+ // 1. The validator flags guides containing banned terms in positive-instruction context
6
+ // 2. The validator passes guides that mention banned terms only as warnings/negations
7
+ // 3. The validator flags unknown clauth key names
8
+ // 4. The validator passes known clauth key names
9
+ //
10
+ // Usage:
11
+ // node scripts/test-guide-validator.mjs
12
+ //
13
+ // Exit codes:
14
+ // 0 = all assertions pass
15
+ // 1 = one or more assertions failed
16
+
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const REPO_ROOT = resolve(__dirname, "..");
22
+
23
+ // Import the validator functions from self-test.mjs by running it in test mode.
24
+ // Since self-test.mjs is not a module that exports cleanly, we replicate the
25
+ // validator logic here using the same constants and algorithms.
26
+ // (The shared implementation lives in runGuideContentValidator / auditGuideFile.)
27
+ //
28
+ // We import via dynamic import of the self-test module's exported helpers —
29
+ // but since self-test.mjs has no exports (it runs immediately), we inline
30
+ // the equivalent test using the same fixture files it targets.
31
+
32
+ // ─── Inline validator (mirrors self-test.mjs logic) ────────────────────────
33
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
34
+
35
+ const GUIDE_BANNED_TERMS = [
36
+ "@masonator/coolify-mcp",
37
+ "@masonator",
38
+ "coolify-mcp",
39
+ "@regen/brand-studio",
40
+ "brand-studio",
41
+ ];
42
+
43
+ const GUIDE_NEGATION_PATTERNS = [
44
+ /\bdo not\b/i,
45
+ /\bnever\b/i,
46
+ /\bno such\b/i,
47
+ /\bdoes not exist\b/i,
48
+ /\bbanned\b/i,
49
+ /\bnot reference\b/i,
50
+ /\bnot use\b/i,
51
+ /\bavoid\b/i,
52
+ /\bremoved\b/i,
53
+ /\bdeprecated\b/i,
54
+ // Markdown table row showing a WRONG→CORRECT mapping (naming-corrections.md pattern)
55
+ /^\|[^|]*WRONG[^|]*\|/i,
56
+ // A table row where the term is in the WRONG column (first data column after the | WRONG | header)
57
+ /^\|\s*(Brand Studio|brand-studio|@regen\/brand-studio|@masonator[^ |]*|coolify-mcp)[^|]*\|\s*\*\*/,
58
+ ];
59
+
60
+ const KNOWN_CLAUTH_KEYS = new Set([
61
+ "coolify-api",
62
+ "cloudflare",
63
+ "npm",
64
+ "supabase",
65
+ "supabase-anon",
66
+ "supabase-db",
67
+ "supabase-service",
68
+ "supabase-service-role",
69
+ "r2-access-key-id",
70
+ "r2-secret-key",
71
+ "anthropic",
72
+ "openai",
73
+ "github",
74
+ "github-token",
75
+ "vultr-dev-ssh",
76
+ "mcp-web-research-secret",
77
+ "mcp-regen-media-secret",
78
+ "web-research",
79
+ "regen-media",
80
+ ]);
81
+
82
+ function auditGuideFile(filepath, relPath) {
83
+ const findings = [];
84
+ let text;
85
+ try {
86
+ text = readFileSync(filepath, "utf8");
87
+ } catch (e) {
88
+ findings.push({ level: "error", code: "guide-unreadable", file: relPath, line: 0, message: `cannot read: ${e.message}` });
89
+ return findings;
90
+ }
91
+ const lines = text.split(/\r?\n/);
92
+ lines.forEach((line, i) => {
93
+ const lineNo = i + 1;
94
+ for (const term of GUIDE_BANNED_TERMS) {
95
+ if (line.includes(term)) {
96
+ const isNegated = GUIDE_NEGATION_PATTERNS.some((re) => re.test(line));
97
+ if (!isNegated) {
98
+ findings.push({ level: "error", code: "guide-banned-term", file: relPath, line: lineNo, message: `banned term "${term}"` });
99
+ }
100
+ }
101
+ }
102
+ const clauthKeyRe = /\bhttp:\/\/127\.0\.0\.1:52437\/v\/([\w-]+)/g;
103
+ let m;
104
+ while ((m = clauthKeyRe.exec(line)) !== null) {
105
+ const key = m[1];
106
+ if (!KNOWN_CLAUTH_KEYS.has(key)) {
107
+ findings.push({ level: "warn", code: "guide-clauth-key-unknown", file: relPath, line: lineNo, message: `unknown clauth key "${key}"` });
108
+ }
109
+ }
110
+ });
111
+ return findings;
112
+ }
113
+
114
+ function scanDir(dir, prefix) {
115
+ const findings = [];
116
+ if (!existsSync(dir)) return findings;
117
+ let entries;
118
+ try { entries = readdirSync(dir); } catch { return findings; }
119
+ for (const entry of entries) {
120
+ const fullPath = join(dir, entry);
121
+ let stat;
122
+ try { stat = statSync(fullPath); } catch { continue; }
123
+ if (stat.isDirectory()) {
124
+ findings.push(...scanDir(fullPath, `${prefix}/${entry}`));
125
+ } else if (entry.endsWith(".md")) {
126
+ findings.push(...auditGuideFile(fullPath, `${prefix}/${entry}`));
127
+ }
128
+ }
129
+ return findings;
130
+ }
131
+
132
+ // ─── Test runner ─────────────────────────────────────────────────────────────
133
+
134
+ let passed = 0;
135
+ let failed = 0;
136
+
137
+ function assert(description, condition, detail = "") {
138
+ if (condition) {
139
+ console.log(` ✓ ${description}`);
140
+ passed++;
141
+ } else {
142
+ console.error(` ✗ FAIL: ${description}${detail ? ` — ${detail}` : ""}`);
143
+ failed++;
144
+ }
145
+ }
146
+
147
+ console.log("\nrdc:self-test — guide-content validator unit tests\n");
148
+
149
+ // ─── Test 1: bad-guide.md should produce errors for banned terms ─────────────
150
+ console.log("Test 1: bad-guide.md flags banned terms");
151
+ const BAD_FIXTURE = join(REPO_ROOT, "scripts/fixtures/guides/bad-guide.md");
152
+ const badFindings = auditGuideFile(BAD_FIXTURE, "fixtures/guides/bad-guide.md");
153
+ const badErrors = badFindings.filter((f) => f.level === "error" && f.code === "guide-banned-term");
154
+ assert("bad-guide.md produces at least 2 banned-term errors", badErrors.length >= 2, `got ${badErrors.length}`);
155
+ assert("flags @masonator/coolify-mcp", badErrors.some((f) => f.message.includes("@masonator/coolify-mcp")));
156
+ assert("flags brand-studio or @regen/brand-studio", badErrors.some((f) => f.message.includes("brand-studio")));
157
+
158
+ // ─── Test 2: bad-guide.md flags unknown clauth key ───────────────────────────
159
+ console.log("\nTest 2: bad-guide.md flags unknown clauth key");
160
+ const badKeyWarnings = badFindings.filter((f) => f.code === "guide-clauth-key-unknown");
161
+ assert("bad-guide.md warns on unknown clauth key 'nonexistent-key'", badKeyWarnings.length >= 1, `got ${badKeyWarnings.length}`);
162
+ assert("warning message references nonexistent-key", badKeyWarnings.some((f) => f.message.includes("nonexistent-key")));
163
+
164
+ // ─── Test 3: good-guide.md produces no errors (negated mentions are OK) ──────
165
+ console.log("\nTest 3: good-guide.md passes (negated mentions are not flagged)");
166
+ const GOOD_FIXTURE = join(REPO_ROOT, "scripts/fixtures/guides-clean/good-guide.md");
167
+ const goodFindings = auditGuideFile(GOOD_FIXTURE, "fixtures/guides-clean/good-guide.md");
168
+ const goodErrors = goodFindings.filter((f) => f.level === "error");
169
+ assert("good-guide.md produces 0 banned-term errors", goodErrors.length === 0, `got ${goodErrors.length}: ${goodErrors.map((f) => f.message).join("; ")}`);
170
+
171
+ // good-guide uses coolify-api which is a known key — no key warnings
172
+ const goodKeyWarnings = goodFindings.filter((f) => f.code === "guide-clauth-key-unknown");
173
+ assert("good-guide.md produces 0 unknown-key warnings", goodKeyWarnings.length === 0, `got ${goodKeyWarnings.length}`);
174
+
175
+ // ─── Test 4: scan fixture directory returns combined findings ─────────────────
176
+ console.log("\nTest 4: scanDir finds findings across fixture dirs");
177
+ const BAD_DIR = join(REPO_ROOT, "scripts/fixtures/guides");
178
+ const dirFindings = scanDir(BAD_DIR, "fixtures");
179
+ const dirErrors = dirFindings.filter((f) => f.level === "error");
180
+ assert("scanDir on bad fixture dir returns errors", dirErrors.length >= 2, `got ${dirErrors.length}`);
181
+
182
+ const GOOD_DIR = join(REPO_ROOT, "scripts/fixtures/guides-clean");
183
+ const cleanFindings = scanDir(GOOD_DIR, "fixtures");
184
+ const cleanErrors = cleanFindings.filter((f) => f.level === "error");
185
+ assert("scanDir on clean fixture dir returns 0 errors", cleanErrors.length === 0, `got ${cleanErrors.length}`);
186
+
187
+ // ─── Summary ─────────────────────────────────────────────────────────────────
188
+ console.log(`\n${"─".repeat(60)}`);
189
+ console.log(`guide-validator tests: ${passed} passed, ${failed} failed`);
190
+ if (failed > 0) {
191
+ console.error(`\n❌ FAIL — ${failed} assertion(s) did not pass`);
192
+ process.exit(1);
193
+ } else {
194
+ console.log(`\n✓ PASS — all guide-content validator assertions passed`);
195
+ process.exit(0);
196
+ }