thumbgate 0.9.9

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 (369) hide show
  1. package/.claude-plugin/README.md +134 -0
  2. package/.claude-plugin/bundle/icon.png +0 -0
  3. package/.claude-plugin/bundle/icon.svg +18 -0
  4. package/.claude-plugin/bundle/server/index.js +24 -0
  5. package/.claude-plugin/marketplace.json +36 -0
  6. package/.claude-plugin/plugin.json +21 -0
  7. package/.well-known/mcp/server-card.json +231 -0
  8. package/LICENSE +21 -0
  9. package/README.md +375 -0
  10. package/adapters/README.md +9 -0
  11. package/adapters/amp/skills/rlhf-feedback/SKILL.md +22 -0
  12. package/adapters/chatgpt/INSTALL.md +83 -0
  13. package/adapters/chatgpt/openapi.yaml +1281 -0
  14. package/adapters/claude/.mcp.json +14 -0
  15. package/adapters/codex/config.toml +9 -0
  16. package/adapters/gemini/function-declarations.json +224 -0
  17. package/adapters/mcp/server-stdio.js +788 -0
  18. package/adapters/opencode/opencode.json +15 -0
  19. package/bin/cli.js +1483 -0
  20. package/bin/memory.sh +64 -0
  21. package/bin/obsidian-sync.sh +20 -0
  22. package/bin/postinstall.js +37 -0
  23. package/config/build-metadata.json +4 -0
  24. package/config/e2e-critical-flows.json +45 -0
  25. package/config/gate-templates.json +77 -0
  26. package/config/gates/claim-verification.json +29 -0
  27. package/config/gates/computer-use.json +39 -0
  28. package/config/gates/default.json +117 -0
  29. package/config/github-about.json +25 -0
  30. package/config/mcp-allowlists.json +135 -0
  31. package/config/model-tiers.json +33 -0
  32. package/config/partner-routing.json +132 -0
  33. package/config/policy-bundles/constrained-v1.json +64 -0
  34. package/config/policy-bundles/default-v1.json +91 -0
  35. package/config/rubrics/default-v1.json +52 -0
  36. package/config/skill-packs/react-testing.json +23 -0
  37. package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
  38. package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
  39. package/config/skill-specs/pr-reviewer.json +9 -0
  40. package/config/skill-specs/release-status.json +9 -0
  41. package/config/skill-specs/ticket-triage.json +9 -0
  42. package/config/subagent-profiles.json +32 -0
  43. package/config/tessl-tiles.json +29 -0
  44. package/config/thumbgate-settings.managed.json +12 -0
  45. package/openapi/openapi.yaml +1281 -0
  46. package/package.json +286 -0
  47. package/plugins/amp-skill/INSTALL.md +52 -0
  48. package/plugins/amp-skill/SKILL.md +64 -0
  49. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
  50. package/plugins/claude-codex-bridge/.mcp.json +12 -0
  51. package/plugins/claude-codex-bridge/INSTALL.md +43 -0
  52. package/plugins/claude-codex-bridge/README.md +46 -0
  53. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
  54. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
  55. package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
  56. package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
  57. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
  58. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
  59. package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
  60. package/plugins/claude-skill/INSTALL.md +55 -0
  61. package/plugins/claude-skill/SKILL.md +46 -0
  62. package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
  63. package/plugins/codex-profile/.mcp.json +12 -0
  64. package/plugins/codex-profile/AGENTS.md +20 -0
  65. package/plugins/codex-profile/INSTALL.md +66 -0
  66. package/plugins/codex-profile/README.md +37 -0
  67. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
  68. package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
  69. package/plugins/cursor-marketplace/LICENSE +21 -0
  70. package/plugins/cursor-marketplace/README.md +124 -0
  71. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
  72. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  73. package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
  74. package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
  75. package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
  76. package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
  77. package/plugins/cursor-marketplace/mcp.json +12 -0
  78. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
  79. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
  80. package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
  81. package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
  82. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
  83. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
  84. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
  85. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
  86. package/plugins/gemini-extension/INSTALL.md +92 -0
  87. package/plugins/gemini-extension/gemini_prompt.txt +14 -0
  88. package/plugins/gemini-extension/tool_contract.json +45 -0
  89. package/plugins/opencode-profile/INSTALL.md +57 -0
  90. package/public/assets/instagram-card.png +0 -0
  91. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  92. package/public/blog.html +400 -0
  93. package/public/dashboard.html +1093 -0
  94. package/public/guide.html +317 -0
  95. package/public/index.html +1195 -0
  96. package/public/learn/agent-harness-pattern.html +180 -0
  97. package/public/learn/ai-agent-persistent-memory.html +202 -0
  98. package/public/learn/learn.css +45 -0
  99. package/public/learn/mcp-pre-action-gates-explained.html +172 -0
  100. package/public/learn/stop-ai-agent-force-push.html +134 -0
  101. package/public/learn/vibe-coding-safety-net.html +142 -0
  102. package/public/learn.html +213 -0
  103. package/public/lessons.html +650 -0
  104. package/public/vercel.json +8 -0
  105. package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
  106. package/scripts/a2ui-engine.js +73 -0
  107. package/scripts/access-anomaly-detector.js +12 -0
  108. package/scripts/adk-consolidator.js +266 -0
  109. package/scripts/agent-readiness.js +220 -0
  110. package/scripts/agent-security-hardening.js +227 -0
  111. package/scripts/agentic-data-pipeline.js +847 -0
  112. package/scripts/analytics-report.js +328 -0
  113. package/scripts/analytics-window.js +158 -0
  114. package/scripts/async-job-runner.js +1001 -0
  115. package/scripts/audit-trail.js +398 -0
  116. package/scripts/auto-promote-gates.js +293 -0
  117. package/scripts/auto-wire-hooks.js +316 -0
  118. package/scripts/autonomous-sales-agent.js +39 -0
  119. package/scripts/autoresearch-runner.js +216 -0
  120. package/scripts/background-agent-governance.js +237 -0
  121. package/scripts/behavioral-extraction.js +93 -0
  122. package/scripts/belief-update.js +84 -0
  123. package/scripts/billing.js +2438 -0
  124. package/scripts/bot-detector.js +50 -0
  125. package/scripts/budget-guard.js +173 -0
  126. package/scripts/build-claude-mcpb.js +189 -0
  127. package/scripts/build-metadata.js +97 -0
  128. package/scripts/check-congruence.js +322 -0
  129. package/scripts/cli-feedback.js +135 -0
  130. package/scripts/cli-telemetry.js +87 -0
  131. package/scripts/cloudflare-dynamic-sandbox.js +315 -0
  132. package/scripts/code-reasoning.js +350 -0
  133. package/scripts/codegraph-context.js +466 -0
  134. package/scripts/commercial-offer.js +56 -0
  135. package/scripts/computer-use-firewall.js +250 -0
  136. package/scripts/context-engine.js +694 -0
  137. package/scripts/contextfs.js +1287 -0
  138. package/scripts/conversation-context.js +119 -0
  139. package/scripts/creator-campaigns.js +239 -0
  140. package/scripts/daemon-manager.js +108 -0
  141. package/scripts/daily-digest.js +11 -0
  142. package/scripts/dashboard-render-spec.js +395 -0
  143. package/scripts/dashboard.js +1058 -0
  144. package/scripts/data-governance.js +173 -0
  145. package/scripts/delegation-runtime.js +900 -0
  146. package/scripts/deploy-gcp.sh +44 -0
  147. package/scripts/deploy-policy.js +231 -0
  148. package/scripts/disagreement-mining.js +315 -0
  149. package/scripts/dispatch-brief.js +159 -0
  150. package/scripts/distribution-surfaces.js +44 -0
  151. package/scripts/dpo-optimizer.js +206 -0
  152. package/scripts/ensure-repo-bootstrap.js +129 -0
  153. package/scripts/ephemeral-agent-store.js +219 -0
  154. package/scripts/eval-harness.js +56 -0
  155. package/scripts/evolution-state.js +241 -0
  156. package/scripts/experiment-tracker.js +267 -0
  157. package/scripts/export-databricks-bundle.js +242 -0
  158. package/scripts/export-dpo-pairs.js +344 -0
  159. package/scripts/export-kto-pairs.js +309 -0
  160. package/scripts/export-training.js +450 -0
  161. package/scripts/failure-diagnostics.js +558 -0
  162. package/scripts/feedback-attribution.js +313 -0
  163. package/scripts/feedback-fallback.js +110 -0
  164. package/scripts/feedback-history-distiller.js +391 -0
  165. package/scripts/feedback-inbox-read.js +162 -0
  166. package/scripts/feedback-loop.js +1887 -0
  167. package/scripts/feedback-paths.js +145 -0
  168. package/scripts/feedback-quality.js +139 -0
  169. package/scripts/feedback-root-consolidator.js +238 -0
  170. package/scripts/feedback-schema.js +426 -0
  171. package/scripts/feedback-session.js +286 -0
  172. package/scripts/feedback-to-memory.js +185 -0
  173. package/scripts/feedback-to-rules.js +164 -0
  174. package/scripts/filesystem-search.js +405 -0
  175. package/scripts/funnel-analytics.js +35 -0
  176. package/scripts/gate-satisfy.js +42 -0
  177. package/scripts/gate-stats.js +116 -0
  178. package/scripts/gate-templates.js +70 -0
  179. package/scripts/gates-engine.js +816 -0
  180. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  181. package/scripts/generate-pretool-hook.sh +40 -0
  182. package/scripts/github-about.js +350 -0
  183. package/scripts/github-outreach.js +65 -0
  184. package/scripts/gtm-revenue-loop.js +520 -0
  185. package/scripts/hallucination-detector.js +226 -0
  186. package/scripts/hf-papers.js +317 -0
  187. package/scripts/history-distiller.js +200 -0
  188. package/scripts/hook-auto-capture.sh +100 -0
  189. package/scripts/hook-stop-pr-thread-check.sh +68 -0
  190. package/scripts/hook-stop-self-score.sh +51 -0
  191. package/scripts/hook-stop-verify-deploy.sh +31 -0
  192. package/scripts/hook-thumbgate-cache-updater.js +48 -0
  193. package/scripts/hook-verify-before-done.sh +20 -0
  194. package/scripts/hosted-config.js +156 -0
  195. package/scripts/hybrid-feedback-context.js +675 -0
  196. package/scripts/install-mcp.js +159 -0
  197. package/scripts/intent-router.js +392 -0
  198. package/scripts/internal-agent-bootstrap.js +490 -0
  199. package/scripts/jsonl-watcher.js +155 -0
  200. package/scripts/lesson-db.js +613 -0
  201. package/scripts/lesson-inference.js +310 -0
  202. package/scripts/lesson-retrieval.js +95 -0
  203. package/scripts/lesson-rotation.js +137 -0
  204. package/scripts/lesson-search.js +644 -0
  205. package/scripts/lesson-synthesis.js +196 -0
  206. package/scripts/license.js +50 -0
  207. package/scripts/local-model-profile.js +384 -0
  208. package/scripts/markdown-escape.js +12 -0
  209. package/scripts/marketing-experiment.js +671 -0
  210. package/scripts/mcp-config.js +149 -0
  211. package/scripts/mcp-policy.js +99 -0
  212. package/scripts/memalign-recall.js +111 -0
  213. package/scripts/memory-firewall.js +222 -0
  214. package/scripts/memory-migration.js +296 -0
  215. package/scripts/meta-policy.js +190 -0
  216. package/scripts/metered-billing.js +16 -0
  217. package/scripts/model-tier-router.js +301 -0
  218. package/scripts/money-watcher.js +71 -0
  219. package/scripts/multi-hop-recall.js +240 -0
  220. package/scripts/natural-language-harness.js +330 -0
  221. package/scripts/obsidian-export.js +713 -0
  222. package/scripts/operational-dashboard.js +103 -0
  223. package/scripts/operational-summary.js +93 -0
  224. package/scripts/optimize-context.js +17 -0
  225. package/scripts/org-dashboard.js +201 -0
  226. package/scripts/partner-orchestration.js +146 -0
  227. package/scripts/per-step-scoring.js +165 -0
  228. package/scripts/perplexity-marketing.js +466 -0
  229. package/scripts/pii-scanner.js +153 -0
  230. package/scripts/plan-gate.js +154 -0
  231. package/scripts/post-everywhere.js +308 -0
  232. package/scripts/post-to-x-retry.sh +22 -0
  233. package/scripts/post-to-x.js +369 -0
  234. package/scripts/pr-manager.js +236 -0
  235. package/scripts/predictive-insights.js +356 -0
  236. package/scripts/principle-extractor.js +162 -0
  237. package/scripts/pro-features.js +40 -0
  238. package/scripts/pro-local-dashboard.js +174 -0
  239. package/scripts/problem-detail.js +53 -0
  240. package/scripts/product-feedback.js +134 -0
  241. package/scripts/profile-router.js +245 -0
  242. package/scripts/prompt-dlp.js +221 -0
  243. package/scripts/prompt-guard.js +83 -0
  244. package/scripts/prove-adapters.js +863 -0
  245. package/scripts/prove-attribution.js +365 -0
  246. package/scripts/prove-automation.js +653 -0
  247. package/scripts/prove-autoresearch.js +304 -0
  248. package/scripts/prove-claim-verification.js +277 -0
  249. package/scripts/prove-cloudflare-sandbox.js +163 -0
  250. package/scripts/prove-data-pipeline.js +410 -0
  251. package/scripts/prove-data-quality.js +227 -0
  252. package/scripts/prove-evolution.js +352 -0
  253. package/scripts/prove-harnesses.js +287 -0
  254. package/scripts/prove-intelligence.js +259 -0
  255. package/scripts/prove-lancedb.js +371 -0
  256. package/scripts/prove-local-intelligence.js +342 -0
  257. package/scripts/prove-loop-closure.js +263 -0
  258. package/scripts/prove-predictive-insights.js +357 -0
  259. package/scripts/prove-runtime.js +350 -0
  260. package/scripts/prove-seo-gsd.js +234 -0
  261. package/scripts/prove-settings.js +279 -0
  262. package/scripts/prove-subway-upgrades.js +277 -0
  263. package/scripts/prove-tessl.js +229 -0
  264. package/scripts/prove-training-export.js +327 -0
  265. package/scripts/prove-workflow-contract.js +116 -0
  266. package/scripts/prove-xmemory.js +332 -0
  267. package/scripts/publish-decision.js +133 -0
  268. package/scripts/pulse.js +80 -0
  269. package/scripts/rate-limiter.js +125 -0
  270. package/scripts/reddit-dm-outreach.js +182 -0
  271. package/scripts/reddit-monitor-cron.sh +26 -0
  272. package/scripts/reflector-agent.js +221 -0
  273. package/scripts/reminder-engine.js +132 -0
  274. package/scripts/revenue-status.js +472 -0
  275. package/scripts/risk-scorer.js +459 -0
  276. package/scripts/rlaif-self-audit.js +129 -0
  277. package/scripts/rlhf_session_start.sh +32 -0
  278. package/scripts/rubric-engine.js +230 -0
  279. package/scripts/schedule-manager.js +251 -0
  280. package/scripts/secret-scanner.js +414 -0
  281. package/scripts/self-heal.js +147 -0
  282. package/scripts/self-healing-check.js +188 -0
  283. package/scripts/semantic-layer.js +98 -0
  284. package/scripts/seo-gsd.js +1153 -0
  285. package/scripts/settings-hierarchy.js +214 -0
  286. package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
  287. package/scripts/skill-exporter.js +262 -0
  288. package/scripts/skill-generator.js +446 -0
  289. package/scripts/skill-materializer.js +134 -0
  290. package/scripts/skill-packs.js +136 -0
  291. package/scripts/skill-proposer.js +99 -0
  292. package/scripts/skill-quality-tracker.js +282 -0
  293. package/scripts/slo-alert-engine.js +14 -0
  294. package/scripts/slow-loop.js +72 -0
  295. package/scripts/social-analytics/db/schema.sql +32 -0
  296. package/scripts/social-analytics/db/social-analytics.db +0 -0
  297. package/scripts/social-analytics/digest.js +256 -0
  298. package/scripts/social-analytics/generate-instagram-card.js +97 -0
  299. package/scripts/social-analytics/instagram-thumbgate-post.js +107 -0
  300. package/scripts/social-analytics/load-env.js +46 -0
  301. package/scripts/social-analytics/mcp-server.js +289 -0
  302. package/scripts/social-analytics/normalizer.js +580 -0
  303. package/scripts/social-analytics/notify.js +162 -0
  304. package/scripts/social-analytics/poll-all.js +92 -0
  305. package/scripts/social-analytics/pollers/github.js +195 -0
  306. package/scripts/social-analytics/pollers/instagram.js +253 -0
  307. package/scripts/social-analytics/pollers/linkedin.js +330 -0
  308. package/scripts/social-analytics/pollers/plausible.js +247 -0
  309. package/scripts/social-analytics/pollers/reddit.js +306 -0
  310. package/scripts/social-analytics/pollers/threads.js +233 -0
  311. package/scripts/social-analytics/pollers/tiktok.js +203 -0
  312. package/scripts/social-analytics/pollers/x.js +227 -0
  313. package/scripts/social-analytics/pollers/youtube.js +304 -0
  314. package/scripts/social-analytics/pollers/zernio.js +183 -0
  315. package/scripts/social-analytics/publish-instagram-thumbgate.js +98 -0
  316. package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
  317. package/scripts/social-analytics/publishers/devto.js +122 -0
  318. package/scripts/social-analytics/publishers/instagram.js +317 -0
  319. package/scripts/social-analytics/publishers/linkedin.js +294 -0
  320. package/scripts/social-analytics/publishers/reddit.js +390 -0
  321. package/scripts/social-analytics/publishers/threads.js +275 -0
  322. package/scripts/social-analytics/publishers/tiktok.js +217 -0
  323. package/scripts/social-analytics/publishers/x.js +259 -0
  324. package/scripts/social-analytics/publishers/youtube.js +223 -0
  325. package/scripts/social-analytics/publishers/zernio.js +378 -0
  326. package/scripts/social-analytics/run-digest.js +34 -0
  327. package/scripts/social-analytics/store.js +257 -0
  328. package/scripts/social-analytics/utm.js +143 -0
  329. package/scripts/social-pipeline.js +2628 -0
  330. package/scripts/social-quality-gate.js +18 -0
  331. package/scripts/social-reply-monitor.js +445 -0
  332. package/scripts/status-dashboard.js +155 -0
  333. package/scripts/statusline-lesson.js +16 -0
  334. package/scripts/statusline-tower.js +8 -0
  335. package/scripts/statusline.sh +116 -0
  336. package/scripts/stripe-live-status.js +115 -0
  337. package/scripts/subagent-profiles.js +79 -0
  338. package/scripts/sync-gh-secrets-from-env.sh +70 -0
  339. package/scripts/sync-github-about.js +52 -0
  340. package/scripts/sync-version.js +447 -0
  341. package/scripts/synthetic-dpo.js +234 -0
  342. package/scripts/telemetry-analytics.js +821 -0
  343. package/scripts/tessl-export.js +371 -0
  344. package/scripts/test-coverage.js +120 -0
  345. package/scripts/thompson-sampling.js +417 -0
  346. package/scripts/thumbgate-search.js +189 -0
  347. package/scripts/tool-kpi-tracker.js +12 -0
  348. package/scripts/tool-registry.js +811 -0
  349. package/scripts/train_from_feedback.py +933 -0
  350. package/scripts/user-profile.js +78 -0
  351. package/scripts/validate-feedback.js +581 -0
  352. package/scripts/validate-workflow-contract.js +287 -0
  353. package/scripts/vector-store.js +197 -0
  354. package/scripts/verification-loop.js +291 -0
  355. package/scripts/verify-obsidian-setup.sh +269 -0
  356. package/scripts/verify-run.js +269 -0
  357. package/scripts/webhook-delivery.js +62 -0
  358. package/scripts/weekly-auto-post.js +124 -0
  359. package/scripts/workflow-runs.js +154 -0
  360. package/scripts/workflow-sprint-intake.js +475 -0
  361. package/scripts/workspace-evolver.js +374 -0
  362. package/scripts/x-autonomous-marketing.js +139 -0
  363. package/scripts/xmemory-lite.js +405 -0
  364. package/skills/agent-memory/SKILL.md +97 -0
  365. package/skills/rlhf-feedback/SKILL.md +49 -0
  366. package/skills/solve-architecture-autonomy/SKILL.md +17 -0
  367. package/skills/solve-architecture-autonomy/tool.js +33 -0
  368. package/skills/thumbgate/SKILL.md +114 -0
  369. package/src/api/server.js +4206 -0
@@ -0,0 +1,1153 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const ROOT = path.join(__dirname, '..');
7
+ const DEFAULT_OUTPUT_DIR = path.join(ROOT, 'docs', 'seo-gsd');
8
+
9
+ const PRODUCT = {
10
+ name: 'ThumbGate',
11
+ npm: 'thumbgate',
12
+ repoUrl: 'https://github.com/IgorGanapolsky/ThumbGate',
13
+ homepageUrl: 'https://thumbgate-production.up.railway.app',
14
+ verificationUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md',
15
+ automationUrl: 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/proof/automation/report.json',
16
+ compatibility: ['Claude Code', 'Cursor', 'Codex', 'Gemini', 'Amp', 'OpenCode'],
17
+ proofPoints: [
18
+ 'thumbs-up/down feedback loop',
19
+ 'pre-action gates',
20
+ 'verification evidence',
21
+ 'automation proof',
22
+ 'SQLite+FTS5 lesson DB',
23
+ 'Thompson Sampling',
24
+ ],
25
+ };
26
+
27
+ const HIGH_ROI_QUERY_SEEDS = [
28
+ {
29
+ query: 'thumbgate vs speclock',
30
+ businessValue: 100,
31
+ source: 'seed',
32
+ notes: 'Bottom-of-funnel comparison against manual spec alternatives.',
33
+ },
34
+ {
35
+ query: 'thumbgate vs mem0',
36
+ businessValue: 98,
37
+ source: 'seed',
38
+ notes: 'Bottom-of-funnel comparison against memory-only tooling.',
39
+ },
40
+ {
41
+ query: 'pre-action gates for ai coding agents',
42
+ businessValue: 96,
43
+ source: 'seed',
44
+ notes: 'Category-defining query that explains the core wedge.',
45
+ },
46
+ {
47
+ query: 'thumbs up thumbs down feedback for ai coding agents',
48
+ businessValue: 95,
49
+ source: 'seed',
50
+ notes: 'Differentiates the explicit feedback loop and aligns with the brand.',
51
+ },
52
+ {
53
+ query: 'claude code feedback memory',
54
+ businessValue: 92,
55
+ source: 'seed',
56
+ notes: 'Agent-specific workflow page with high compatibility intent.',
57
+ },
58
+ {
59
+ query: 'ai coding agent guardrails',
60
+ businessValue: 90,
61
+ source: 'seed',
62
+ notes: 'Broader category demand that feeds comparison and guide pages.',
63
+ },
64
+ {
65
+ query: 'stop ai coding agents from repeating mistakes',
66
+ businessValue: 88,
67
+ source: 'seed',
68
+ notes: 'Problem-led copy that maps to landing-page positioning.',
69
+ },
70
+ {
71
+ query: 'claude code prevent repeated mistakes',
72
+ businessValue: 86,
73
+ source: 'seed',
74
+ notes: 'High-intent pain query for Claude Code buyers.',
75
+ },
76
+ ];
77
+
78
+ const PAGE_BLUEPRINTS = [
79
+ {
80
+ query: 'thumbgate vs speclock',
81
+ path: '/compare/speclock',
82
+ pageType: 'comparison',
83
+ pillar: 'comparison',
84
+ title: 'ThumbGate vs SpecLock | Thumbs Feedback vs Manual Specs',
85
+ heroTitle: 'ThumbGate vs SpecLock',
86
+ heroSummary: 'SpecLock starts from manually written constraints. ThumbGate starts from thumbs-up/down feedback and turns it into pre-action gates that block repeated mistakes.',
87
+ takeaways: [
88
+ 'ThumbGate learns from thumbs-up and thumbs-down feedback without requiring a separate spec-writing workflow.',
89
+ 'SpecLock is strongest when a team already has strong specifications and wants enforcement tied to those documents.',
90
+ 'ThumbGate is strongest when the pain is repeated agent mistakes across Claude Code, Cursor, Codex, Gemini, Amp, and OpenCode.',
91
+ ],
92
+ sections: [
93
+ {
94
+ heading: 'The product difference in one sentence',
95
+ paragraphs: [
96
+ 'SpecLock helps a team codify rules before the work begins. ThumbGate helps a team convert real thumbs-up/down feedback into live pre-action gates after the work reveals what actually breaks.',
97
+ 'That means ThumbGate is better for fast-moving agent workflows where the problem is not writing more specs, but preventing the same mistake from happening again tomorrow.',
98
+ ],
99
+ },
100
+ {
101
+ heading: 'Choose ThumbGate when',
102
+ bullets: [
103
+ 'Your agent already repeats known mistakes and you need the block to happen before tool execution.',
104
+ 'You want one feedback loop that supports both reinforcement from thumbs up and prevention from thumbs down.',
105
+ 'You need proof assets, automation reports, and compatibility across multiple coding agents.',
106
+ ],
107
+ },
108
+ {
109
+ heading: 'Choose SpecLock when',
110
+ bullets: [
111
+ 'Your team already maintains strong PRDs or system specs and wants the model constrained against those artifacts.',
112
+ 'Your primary problem is uncontrolled file edits, not a missing feedback-to-enforcement loop.',
113
+ 'You are willing to invest in manual constraint authoring as part of the workflow.',
114
+ ],
115
+ },
116
+ ],
117
+ faq: [
118
+ {
119
+ question: 'Is ThumbGate trying to replace specs?',
120
+ answer: 'No. ThumbGate complements specs by capturing thumbs-up/down feedback from live agent behavior and enforcing the learned rules as pre-action gates.',
121
+ },
122
+ {
123
+ question: 'What does ThumbGate do that SpecLock does not?',
124
+ answer: 'ThumbGate turns explicit feedback into searchable memory, auto-generated prevention rules, and runtime gates that block repeated mistakes before the next tool call executes.',
125
+ },
126
+ ],
127
+ relatedPaths: ['/compare/mem0', '/guides/pre-action-gates'],
128
+ },
129
+ {
130
+ query: 'thumbgate vs mem0',
131
+ path: '/compare/mem0',
132
+ pageType: 'comparison',
133
+ pillar: 'comparison',
134
+ title: 'ThumbGate vs Mem0 | Enforcement vs Memory for AI Agents',
135
+ heroTitle: 'ThumbGate vs Mem0',
136
+ heroSummary: 'Mem0 is memory. ThumbGate is memory plus enforcement. It captures thumbs-up/down feedback, promotes the signal into rules, and blocks repeat failures with pre-action gates.',
137
+ takeaways: [
138
+ 'Mem0 is useful when you mainly need retrieval and cross-session context.',
139
+ 'ThumbGate is useful when retrieval alone is not enough and the system has to stop the same mistake before execution.',
140
+ 'ThumbGate adds proof assets and automation reports so the buying story is stronger for engineering teams.',
141
+ ],
142
+ sections: [
143
+ {
144
+ heading: 'Where Mem0 fits',
145
+ paragraphs: [
146
+ 'Mem0 is designed as a cloud memory layer. It helps the model remember context and past interactions, but memory alone does not guarantee that the next action is safe.',
147
+ ],
148
+ },
149
+ {
150
+ heading: 'Where ThumbGate fits',
151
+ paragraphs: [
152
+ 'ThumbGate begins with the same need to remember, but it goes further. A thumbs down can become a prevention rule, and that rule can become a pre-action gate that blocks a repeated tool call.',
153
+ ],
154
+ bullets: [
155
+ 'Thumbs up reinforces good behavior.',
156
+ 'Thumbs down blocks repeated mistakes.',
157
+ 'Verification evidence and automation reports back up the reliability claim.',
158
+ ],
159
+ },
160
+ {
161
+ heading: 'Which page should rank',
162
+ paragraphs: [
163
+ 'This comparison page should win when the searcher is already deciding between a memory system and an enforcement system. The goal is to make the distinction obvious in under 30 seconds.',
164
+ ],
165
+ },
166
+ ],
167
+ faq: [
168
+ {
169
+ question: 'Does ThumbGate still include memory?',
170
+ answer: 'Yes. ThumbGate keeps local-first memory, ContextFS packs, lesson search, and recall, but adds pre-action enforcement when memory alone is insufficient.',
171
+ },
172
+ {
173
+ question: 'Why compare Mem0 at all?',
174
+ answer: 'Because buyers often start with memory tooling and only later realize they also need enforcement. This page makes that upgrade path explicit.',
175
+ },
176
+ ],
177
+ relatedPaths: ['/compare/speclock', '/guides/claude-code-feedback'],
178
+ },
179
+ {
180
+ query: 'pre-action gates for ai coding agents',
181
+ path: '/guides/pre-action-gates',
182
+ pageType: 'guide',
183
+ pillar: 'pre-action-gates',
184
+ title: 'Pre-Action Gates for AI Coding Agents | ThumbGate Guide',
185
+ heroTitle: 'What Are Pre-Action Gates?',
186
+ heroSummary: 'Pre-action gates stop the risky move before the agent executes it. ThumbGate uses thumbs-up/down feedback to decide what should be reinforced, warned, or blocked.',
187
+ takeaways: [
188
+ 'Prompt rules are advisory. Pre-action gates are enforcement.',
189
+ 'A repeated thumbs down can become a warning gate or a hard block.',
190
+ 'The right proof asset is not the rule text alone but the evidence that the gate fired before damage.',
191
+ ],
192
+ sections: [
193
+ {
194
+ heading: 'Why this matters',
195
+ paragraphs: [
196
+ 'Most AI coding failures are not mysterious. They are repeated mistakes: force-pushes, destructive scripts, missed verification steps, or breaking architectural constraints.',
197
+ 'A pre-action gate turns that failure pattern into a runtime checkpoint. The agent sees the stop before the bad action lands.',
198
+ ],
199
+ },
200
+ {
201
+ heading: 'How ThumbGate makes the loop useful',
202
+ bullets: [
203
+ 'Capture structured thumbs-up/down feedback.',
204
+ 'Promote repeated failures into prevention rules.',
205
+ 'Score and enforce the rules with Thompson Sampling and pre-action hooks.',
206
+ 'Publish verification evidence so the system is auditable.',
207
+ ],
208
+ },
209
+ {
210
+ heading: 'Best next step',
211
+ paragraphs: [
212
+ 'If a buyer is exploring the category, this page should move them to either a comparison page or the main product proof pack.',
213
+ ],
214
+ },
215
+ ],
216
+ faq: [
217
+ {
218
+ question: 'How are pre-action gates different from prompt rules?',
219
+ answer: 'Prompt rules ask the model nicely. Pre-action gates intercept the tool call and block it before execution when the known-bad pattern matches.',
220
+ },
221
+ {
222
+ question: 'Can a thumbs up matter too?',
223
+ answer: 'Yes. ThumbGate explicitly uses thumbs up to reinforce successful behavior so the system is not only punitive.',
224
+ },
225
+ ],
226
+ relatedPaths: ['/compare/speclock', '/guides/claude-code-feedback'],
227
+ },
228
+ {
229
+ query: 'claude code feedback memory',
230
+ path: '/guides/claude-code-feedback',
231
+ pageType: 'integration',
232
+ pillar: 'agent-workflows',
233
+ title: 'Claude Code Feedback Memory with Thumbs Up and Thumbs Down',
234
+ heroTitle: 'Claude Code Feedback Memory That Actually Enforces',
235
+ heroSummary: 'Claude Code can remember more when the memory is structured, but reliability improves when thumbs-up/down feedback also becomes enforceable behavior. That is ThumbGate\'s angle.',
236
+ takeaways: [
237
+ 'Claude Code users usually feel the pain as repeated mistakes across sessions.',
238
+ 'ThumbGate captures the thumbs-up/down signal and turns it into memory, rules, and gates.',
239
+ 'The page should convert Claude Code searchers into a product trial or a comparison-page reader.',
240
+ ],
241
+ sections: [
242
+ {
243
+ heading: 'The Claude Code problem',
244
+ paragraphs: [
245
+ 'Claude Code is strongest when the context is fresh, but teams still hit repeated mistakes, compaction drift, and re-explaining constraints. A memory file alone helps, but it does not physically stop the next bad move.',
246
+ ],
247
+ },
248
+ {
249
+ heading: 'The ThumbGate angle',
250
+ bullets: [
251
+ 'Thumbs up reinforces good behavior.',
252
+ 'Thumbs down becomes a prevention rule.',
253
+ 'Pre-action gates stop the repeated mistake before the next command executes.',
254
+ 'The same flow works across Cursor, Codex, Gemini, Amp, and OpenCode.',
255
+ ],
256
+ },
257
+ {
258
+ heading: 'What to show on this page',
259
+ paragraphs: [
260
+ 'Compatibility proof, install speed, and verification evidence matter more than generic "memory" copy. The buyer should leave knowing that ThumbGate is the enforcement layer for Claude Code, not just another notebook of past context.',
261
+ ],
262
+ },
263
+ ],
264
+ faq: [
265
+ {
266
+ question: 'Does this only work with Claude Code?',
267
+ answer: 'No. Claude Code is a strong entry point, but the same thumbs-up/down feedback loop and pre-action gates work across other MCP-compatible coding agents too.',
268
+ },
269
+ {
270
+ question: 'Why mention thumbs up as well as thumbs down?',
271
+ answer: 'Because reinforcement matters. Good behavior should become easier to repeat, not only bad behavior harder to repeat.',
272
+ },
273
+ ],
274
+ relatedPaths: ['/guides/pre-action-gates', '/compare/mem0'],
275
+ },
276
+ {
277
+ query: 'claude desktop extension plugin thumbgate',
278
+ path: '/guides/claude-desktop',
279
+ pageType: 'integration',
280
+ pillar: 'agent-workflows',
281
+ title: 'ThumbGate for Claude Desktop | Install the Plugin in 60 Seconds',
282
+ heroTitle: 'ThumbGate for Claude Desktop',
283
+ heroSummary: 'Install ThumbGate as a Claude Desktop plugin and get pre-action gates running in under a minute. No build step, no cloud account, no config files.',
284
+ takeaways: [
285
+ 'One command installs ThumbGate into Claude Desktop with zero config.',
286
+ 'The packaged .mcpb bundle is available on GitHub Releases for drag-and-drop install.',
287
+ 'All feedback, rules, and gates stay local on your machine.',
288
+ ],
289
+ sections: [
290
+ {
291
+ heading: 'Install with one command',
292
+ paragraphs: [
293
+ 'Run this in your terminal and Claude Desktop picks up ThumbGate automatically:',
294
+ 'npx thumbgate init --claude-desktop',
295
+ 'Or add the MCP server directly:',
296
+ 'claude mcp add thumbgate -- npx -y thumbgate serve',
297
+ ],
298
+ },
299
+ {
300
+ heading: 'Or download the packaged bundle',
301
+ paragraphs: [
302
+ 'Grab the .mcpb bundle from GitHub Releases — no build step required. Drop it into Claude Desktop and you are running.',
303
+ ],
304
+ bullets: [
305
+ 'Download from: github.com/IgorGanapolsky/ThumbGate/releases',
306
+ 'Works with Claude Desktop on macOS, Windows, and Linux.',
307
+ 'All data stays local. No cloud account needed.',
308
+ ],
309
+ },
310
+ {
311
+ heading: 'What you get',
312
+ bullets: [
313
+ 'Thumbs-up/down feedback capture inside Claude Desktop.',
314
+ 'Prevention rules auto-generated from repeated failures.',
315
+ 'Pre-action gates that block known-bad patterns before execution.',
316
+ 'Full-text search across your lesson history.',
317
+ 'Health checks and system diagnostics.',
318
+ ],
319
+ },
320
+ {
321
+ heading: 'Verify it works',
322
+ paragraphs: [
323
+ 'After install, run npx thumbgate doctor to confirm all subsystems are healthy. You should see 4/4 HEALTHY.',
324
+ ],
325
+ },
326
+ ],
327
+ faq: [
328
+ {
329
+ question: 'Do I need a cloud account?',
330
+ answer: 'No. ThumbGate runs entirely locally. Your feedback, rules, and gates never leave your machine.',
331
+ },
332
+ {
333
+ question: 'What is the .mcpb bundle?',
334
+ answer: 'It is a packaged Claude Desktop extension that includes the ThumbGate MCP server, tool definitions, and manifest — ready to install without building from source.',
335
+ },
336
+ {
337
+ question: 'Does this work with Claude Code too?',
338
+ answer: 'Yes. The same npx thumbgate init command works for both Claude Desktop and Claude Code. Use --claude-desktop for the Desktop-specific setup.',
339
+ },
340
+ ],
341
+ relatedPaths: ['/guides/claude-code-feedback', '/guides/pre-action-gates'],
342
+ },
343
+ ];
344
+
345
+ function normalizeText(value) {
346
+ return String(value || '').replace(/\s+/g, ' ').trim();
347
+ }
348
+
349
+ function slugify(value) {
350
+ return normalizeText(value)
351
+ .toLowerCase()
352
+ .replace(/[^a-z0-9]+/g, '-')
353
+ .replace(/^-+|-+$/g, '');
354
+ }
355
+
356
+ function toNumber(value) {
357
+ if (value === undefined || value === null || value === '') return null;
358
+ const num = Number(value);
359
+ return Number.isFinite(num) ? num : null;
360
+ }
361
+
362
+ function clamp(value, min, max) {
363
+ return Math.min(Math.max(value, min), max);
364
+ }
365
+
366
+ function escapeHtml(value) {
367
+ return String(value)
368
+ .replaceAll('&', '&')
369
+ .replaceAll('<', '&lt;')
370
+ .replaceAll('>', '&gt;')
371
+ .replaceAll('"', '&quot;')
372
+ .replaceAll("'", '&#39;');
373
+ }
374
+
375
+ function parseCsvLine(line) {
376
+ const cells = [];
377
+ let current = '';
378
+ let inQuotes = false;
379
+
380
+ for (let i = 0; i < line.length; i++) {
381
+ const char = line[i];
382
+ const next = line[i + 1];
383
+ if (char === '"' && inQuotes && next === '"') {
384
+ current += '"';
385
+ i++;
386
+ continue;
387
+ }
388
+ if (char === '"') {
389
+ inQuotes = !inQuotes;
390
+ continue;
391
+ }
392
+ if (char === ',' && !inQuotes) {
393
+ cells.push(current);
394
+ current = '';
395
+ continue;
396
+ }
397
+ current += char;
398
+ }
399
+ cells.push(current);
400
+ return cells.map((cell) => cell.trim());
401
+ }
402
+
403
+ function parseCsv(text) {
404
+ const lines = String(text || '')
405
+ .split(/\r?\n/)
406
+ .map((line) => line.trim())
407
+ .filter(Boolean);
408
+ if (lines.length === 0) return [];
409
+ const headers = parseCsvLine(lines[0]).map((header) => slugify(header).replace(/-/g, '_'));
410
+ return lines.slice(1).map((line) => {
411
+ const values = parseCsvLine(line);
412
+ return Object.fromEntries(headers.map((header, index) => [header, values[index] || '']));
413
+ });
414
+ }
415
+
416
+ function loadKeywordRows(inputPath) {
417
+ if (!inputPath) {
418
+ return HIGH_ROI_QUERY_SEEDS.map((row) => ({ ...row }));
419
+ }
420
+ const resolved = path.resolve(inputPath);
421
+ const raw = fs.readFileSync(resolved, 'utf8');
422
+ if (resolved.endsWith('.json')) {
423
+ const data = JSON.parse(raw);
424
+ return Array.isArray(data) ? data : data.rows || [];
425
+ }
426
+ if (resolved.endsWith('.jsonl')) {
427
+ return raw
428
+ .split(/\r?\n/)
429
+ .filter(Boolean)
430
+ .map((line) => JSON.parse(line));
431
+ }
432
+ if (resolved.endsWith('.csv')) {
433
+ return parseCsv(raw);
434
+ }
435
+ throw new Error(`Unsupported keyword input format for ${resolved}`);
436
+ }
437
+
438
+ function classifyIntent(query) {
439
+ const normalized = normalizeText(query).toLowerCase();
440
+ if (!normalized) return 'informational';
441
+ if (/\b(vs|versus|alternative|compare|comparison|better than)\b/.test(normalized)) return 'comparison';
442
+ if (/\b(price|pricing|buy|checkout|purchase|cost)\b/.test(normalized)) return 'transactional';
443
+ if (/\b(claude code|cursor|codex|gemini|amp|opencode|integration|plugin|setup|install)\b/.test(normalized)) {
444
+ return 'commercial';
445
+ }
446
+ if (/\b(what is|how to|guide|best practices|why)\b/.test(normalized)) return 'informational';
447
+ if (/\b(guardrails|pre-action gates|feedback|prevent repeated mistakes|memory)\b/.test(normalized)) {
448
+ return 'commercial';
449
+ }
450
+ return 'informational';
451
+ }
452
+
453
+ function inferPillar(query) {
454
+ const normalized = normalizeText(query).toLowerCase();
455
+ if (/\b(speclock|mem0|alternative|vs|compare|comparison)\b/.test(normalized)) return 'comparison';
456
+ if (/\b(thumbs up|thumbs down|feedback|reinforce|mistake)\b/.test(normalized)) return 'feedback-loop';
457
+ if (/\b(pre-action gates|guardrails|block|prevent repeated mistakes)\b/.test(normalized)) return 'pre-action-gates';
458
+ if (/\b(claude code|cursor|codex|gemini|amp|opencode|integration|plugin)\b/.test(normalized)) return 'agent-workflows';
459
+ return 'ai-agent-reliability';
460
+ }
461
+
462
+ function inferPersona(query) {
463
+ const normalized = normalizeText(query).toLowerCase();
464
+ if (normalized.includes('claude code')) return 'claude-code-builder';
465
+ if (normalized.includes('cursor')) return 'cursor-builder';
466
+ if (/\b(vs|alternative|compare)\b/.test(normalized)) return 'tool-evaluator';
467
+ if (/\b(guardrails|pre-action gates)\b/.test(normalized)) return 'engineering-lead';
468
+ return 'ai-engineer';
469
+ }
470
+
471
+ function inferPageType(intent, query) {
472
+ const normalized = normalizeText(query).toLowerCase();
473
+ if (intent === 'comparison') return 'comparison';
474
+ if (/\b(claude code|cursor|codex|gemini|amp|opencode|integration|plugin)\b/.test(normalized)) return 'integration';
475
+ if (/\b(guide|how to|what is|best practices)\b/.test(normalized)) return 'guide';
476
+ return intent === 'transactional' ? 'money-page' : 'guide';
477
+ }
478
+
479
+ function scoreOpportunity(row) {
480
+ const query = normalizeText(row.query);
481
+ const intent = row.intent || classifyIntent(query);
482
+ const pillar = row.pillar || inferPillar(query);
483
+ const pageType = row.pageType || inferPageType(intent, query);
484
+ let score = 0;
485
+
486
+ const intentWeight = {
487
+ comparison: 40,
488
+ transactional: 38,
489
+ commercial: 32,
490
+ informational: 24,
491
+ };
492
+ const pageTypeWeight = {
493
+ comparison: 20,
494
+ integration: 16,
495
+ 'money-page': 18,
496
+ guide: 14,
497
+ };
498
+ const pillarWeight = {
499
+ comparison: 14,
500
+ 'pre-action-gates': 12,
501
+ 'feedback-loop': 12,
502
+ 'agent-workflows': 11,
503
+ 'ai-agent-reliability': 9,
504
+ };
505
+
506
+ score += intentWeight[intent] || 20;
507
+ score += pageTypeWeight[pageType] || 12;
508
+ score += pillarWeight[pillar] || 8;
509
+ score += clamp(toNumber(row.businessValue) || 0, 0, 25);
510
+
511
+ const impressions = toNumber(row.impressions);
512
+ const clicks = toNumber(row.clicks);
513
+ const ctr = toNumber(row.ctr);
514
+ const position = toNumber(row.position);
515
+
516
+ if (impressions !== null) score += clamp(impressions / 20, 0, 10);
517
+ if (clicks !== null) score += clamp(clicks, 0, 10);
518
+ if (ctr !== null) score += clamp(ctr * 100, 0, 6);
519
+ if (position !== null) {
520
+ if (position >= 4 && position <= 25) score += 6;
521
+ else if (position > 25) score += 3;
522
+ }
523
+
524
+ if (/\bthumbgate\b/.test(query.toLowerCase())) score += 4;
525
+ if (/\b(claude code|cursor|codex|gemini|amp|opencode)\b/.test(query.toLowerCase())) score += 4;
526
+
527
+ return clamp(Number(score.toFixed(2)), 0, 100);
528
+ }
529
+
530
+ function normalizeKeywordRow(row, index = 0) {
531
+ const query = normalizeText(row.query || row.keyword || row.term || row.topic);
532
+ if (!query) {
533
+ throw new Error(`Keyword row ${index + 1} is missing query/keyword/term/topic`);
534
+ }
535
+
536
+ const normalized = {
537
+ id: row.id || `kw_${index + 1}_${slugify(query)}`,
538
+ query,
539
+ source: normalizeText(row.source) || 'input',
540
+ notes: normalizeText(row.notes) || null,
541
+ impressions: toNumber(row.impressions),
542
+ clicks: toNumber(row.clicks),
543
+ ctr: toNumber(row.ctr),
544
+ position: toNumber(row.position),
545
+ businessValue: toNumber(row.businessValue) || 0,
546
+ };
547
+
548
+ normalized.intent = classifyIntent(normalized.query);
549
+ normalized.pillar = inferPillar(normalized.query);
550
+ normalized.persona = inferPersona(normalized.query);
551
+ normalized.pageType = inferPageType(normalized.intent, normalized.query);
552
+ normalized.opportunityScore = scoreOpportunity(normalized);
553
+ return normalized;
554
+ }
555
+
556
+ function clusterKeywordRows(rows) {
557
+ const clusters = new Map();
558
+
559
+ for (const row of rows) {
560
+ const key = row.pillar;
561
+ if (!clusters.has(key)) {
562
+ clusters.set(key, {
563
+ pillar: key,
564
+ pageType: row.pageType,
565
+ queries: [],
566
+ totalOpportunityScore: 0,
567
+ primaryQuery: null,
568
+ personas: new Set(),
569
+ intents: new Set(),
570
+ });
571
+ }
572
+ const cluster = clusters.get(key);
573
+ cluster.queries.push(row);
574
+ cluster.totalOpportunityScore += row.opportunityScore;
575
+ cluster.personas.add(row.persona);
576
+ cluster.intents.add(row.intent);
577
+ if (!cluster.primaryQuery || row.opportunityScore > cluster.primaryQuery.opportunityScore) {
578
+ cluster.primaryQuery = row;
579
+ cluster.pageType = row.pageType;
580
+ }
581
+ }
582
+
583
+ return [...clusters.values()]
584
+ .map((cluster) => ({
585
+ ...cluster,
586
+ personas: [...cluster.personas].sort(),
587
+ intents: [...cluster.intents].sort(),
588
+ totalOpportunityScore: Number(cluster.totalOpportunityScore.toFixed(2)),
589
+ queries: [...cluster.queries].sort((a, b) => b.opportunityScore - a.opportunityScore),
590
+ }))
591
+ .sort((a, b) => b.totalOpportunityScore - a.totalOpportunityScore);
592
+ }
593
+
594
+ function trimMetaDescription(value, max = 160) {
595
+ const text = normalizeText(value);
596
+ if (text.length <= max) return text;
597
+ return `${text.slice(0, max - 3).trim()}...`;
598
+ }
599
+
600
+ function createPageSpec(blueprint, row) {
601
+ const keywordCluster = clusterKeywordRows(
602
+ HIGH_ROI_QUERY_SEEDS.map((seed, index) => normalizeKeywordRow(seed, index))
603
+ ).find((cluster) => cluster.pillar === blueprint.pillar);
604
+ const description = trimMetaDescription(blueprint.heroSummary);
605
+ const relatedPages = blueprint.relatedPaths.map((relatedPath) => {
606
+ const related = PAGE_BLUEPRINTS.find((candidate) => candidate.path === relatedPath);
607
+ return {
608
+ path: relatedPath,
609
+ title: related ? related.heroTitle : relatedPath,
610
+ };
611
+ });
612
+
613
+ return {
614
+ path: blueprint.path,
615
+ slug: blueprint.path.split('/').filter(Boolean).join('-'),
616
+ query: row.query,
617
+ pillar: row.pillar,
618
+ intent: row.intent,
619
+ pageType: blueprint.pageType,
620
+ persona: row.persona,
621
+ opportunityScore: row.opportunityScore,
622
+ title: blueprint.title,
623
+ description,
624
+ heroTitle: blueprint.heroTitle,
625
+ heroSummary: blueprint.heroSummary,
626
+ takeaways: blueprint.takeaways,
627
+ sections: blueprint.sections,
628
+ faq: blueprint.faq,
629
+ relatedPages,
630
+ cta: {
631
+ label: 'Review verification evidence',
632
+ href: PRODUCT.verificationUrl,
633
+ },
634
+ proofLinks: [
635
+ { label: 'Verification evidence', href: PRODUCT.verificationUrl },
636
+ { label: 'Automation proof', href: PRODUCT.automationUrl },
637
+ { label: 'GitHub repository', href: PRODUCT.repoUrl },
638
+ ],
639
+ changefreq: blueprint.pageType === 'comparison' ? 'weekly' : 'monthly',
640
+ priority: blueprint.pageType === 'comparison' ? '0.9' : '0.8',
641
+ keywordCluster: keywordCluster ? keywordCluster.queries.slice(0, 4).map((item) => item.query) : [row.query],
642
+ imageAlt: `${PRODUCT.name} guide for ${blueprint.heroTitle}`,
643
+ };
644
+ }
645
+
646
+ function buildThumbGateSeoPlan(rawRows = HIGH_ROI_QUERY_SEEDS) {
647
+ const capture = rawRows.map((row, index) => normalizeKeywordRow(row, index));
648
+ const clusters = clusterKeywordRows(capture);
649
+ const rowsByQuery = new Map(capture.map((row) => [row.query.toLowerCase(), row]));
650
+ const pages = PAGE_BLUEPRINTS.map((blueprint) => {
651
+ const row = rowsByQuery.get(blueprint.query.toLowerCase()) || normalizeKeywordRow({
652
+ query: blueprint.query,
653
+ businessValue: 90,
654
+ source: 'blueprint',
655
+ });
656
+ return createPageSpec(blueprint, row);
657
+ }).sort((a, b) => b.opportunityScore - a.opportunityScore);
658
+
659
+ const briefs = pages.map((page, index) => ({
660
+ priority: index + 1,
661
+ path: page.path,
662
+ title: page.title,
663
+ primaryQuery: page.query,
664
+ persona: page.persona,
665
+ pageType: page.pageType,
666
+ opportunityScore: page.opportunityScore,
667
+ cta: page.cta,
668
+ keywordCluster: page.keywordCluster,
669
+ summary: page.heroSummary,
670
+ }));
671
+
672
+ return {
673
+ framework: 'GSD',
674
+ capture: {
675
+ keywordRows: capture,
676
+ totalKeywords: capture.length,
677
+ },
678
+ clarify: {
679
+ intents: capture.reduce((acc, row) => {
680
+ acc[row.intent] = (acc[row.intent] || 0) + 1;
681
+ return acc;
682
+ }, {}),
683
+ personas: capture.reduce((acc, row) => {
684
+ acc[row.persona] = (acc[row.persona] || 0) + 1;
685
+ return acc;
686
+ }, {}),
687
+ pageTypes: capture.reduce((acc, row) => {
688
+ acc[row.pageType] = (acc[row.pageType] || 0) + 1;
689
+ return acc;
690
+ }, {}),
691
+ },
692
+ organize: {
693
+ clusters,
694
+ topClusters: clusters.slice(0, 4),
695
+ },
696
+ execute: {
697
+ briefs,
698
+ pages,
699
+ },
700
+ review: {
701
+ topOpportunityQuery: capture.slice().sort((a, b) => b.opportunityScore - a.opportunityScore)[0],
702
+ recommendedOrder: briefs.map((brief) => brief.path),
703
+ proofAssets: PRODUCT.proofPoints,
704
+ },
705
+ };
706
+ }
707
+
708
+ function renderPlanMarkdown(plan) {
709
+ const lines = [
710
+ '# ThumbGate SEO/GEO GSD Plan',
711
+ '',
712
+ `Framework: ${plan.framework}`,
713
+ '',
714
+ '## Capture',
715
+ '',
716
+ `- Total keyword rows: ${plan.capture.totalKeywords}`,
717
+ ...plan.capture.keywordRows.map((row) => `- ${row.query} | intent=${row.intent} | pillar=${row.pillar} | score=${row.opportunityScore}`),
718
+ '',
719
+ '## Clarify',
720
+ '',
721
+ `- Intents: ${Object.entries(plan.clarify.intents).map(([key, value]) => `${key}=${value}`).join(', ')}`,
722
+ `- Personas: ${Object.entries(plan.clarify.personas).map(([key, value]) => `${key}=${value}`).join(', ')}`,
723
+ `- Page types: ${Object.entries(plan.clarify.pageTypes).map(([key, value]) => `${key}=${value}`).join(', ')}`,
724
+ '',
725
+ '## Organize',
726
+ '',
727
+ ...plan.organize.topClusters.map((cluster) => `- ${cluster.pillar}: ${cluster.primaryQuery.query} (${cluster.totalOpportunityScore})`),
728
+ '',
729
+ '## Execute',
730
+ '',
731
+ ...plan.execute.briefs.map((brief) => (
732
+ `### ${brief.priority}. ${brief.title}\n\n- Path: ${brief.path}\n- Primary query: ${brief.primaryQuery}\n- Persona: ${brief.persona}\n- Page type: ${brief.pageType}\n- Opportunity score: ${brief.opportunityScore}\n- CTA: ${brief.cta.label}\n- Summary: ${brief.summary}`
733
+ )),
734
+ '',
735
+ '## Review',
736
+ '',
737
+ `- Top opportunity query: ${plan.review.topOpportunityQuery.query}`,
738
+ `- Recommended publish order: ${plan.review.recommendedOrder.join(', ')}`,
739
+ `- Proof assets: ${plan.review.proofAssets.join(', ')}`,
740
+ '',
741
+ ];
742
+ return lines.join('\n');
743
+ }
744
+
745
+ function writePlanOutputs(plan, outputDir = DEFAULT_OUTPUT_DIR) {
746
+ fs.mkdirSync(outputDir, { recursive: true });
747
+ const files = {
748
+ capture: path.join(outputDir, '01-capture.json'),
749
+ clarify: path.join(outputDir, '02-clarify.json'),
750
+ organize: path.join(outputDir, '03-organize.json'),
751
+ execute: path.join(outputDir, '04-execute-briefs.md'),
752
+ review: path.join(outputDir, '05-review.json'),
753
+ pages: path.join(outputDir, '06-page-specs.json'),
754
+ };
755
+
756
+ fs.writeFileSync(files.capture, `${JSON.stringify(plan.capture, null, 2)}\n`);
757
+ fs.writeFileSync(files.clarify, `${JSON.stringify(plan.clarify, null, 2)}\n`);
758
+ fs.writeFileSync(files.organize, `${JSON.stringify(plan.organize, null, 2)}\n`);
759
+ fs.writeFileSync(files.execute, `${renderPlanMarkdown(plan)}\n`);
760
+ fs.writeFileSync(files.review, `${JSON.stringify(plan.review, null, 2)}\n`);
761
+ fs.writeFileSync(files.pages, `${JSON.stringify(plan.execute.pages, null, 2)}\n`);
762
+ return files;
763
+ }
764
+
765
+ function renderFaqJsonLd(page) {
766
+ if (!Array.isArray(page.faq) || page.faq.length === 0) return '';
767
+ return JSON.stringify({
768
+ '@context': 'https://schema.org',
769
+ '@type': 'FAQPage',
770
+ mainEntity: page.faq.map((item) => ({
771
+ '@type': 'Question',
772
+ name: item.question,
773
+ acceptedAnswer: {
774
+ '@type': 'Answer',
775
+ text: item.answer,
776
+ },
777
+ })),
778
+ }, null, 2);
779
+ }
780
+
781
+ function renderWebPageJsonLd(page, runtimeConfig) {
782
+ return JSON.stringify({
783
+ '@context': 'https://schema.org',
784
+ '@type': 'TechArticle',
785
+ headline: page.heroTitle,
786
+ description: page.description,
787
+ about: page.keywordCluster,
788
+ url: `${runtimeConfig.appOrigin}${page.path}`,
789
+ publisher: {
790
+ '@type': 'Organization',
791
+ name: PRODUCT.name,
792
+ url: runtimeConfig.appOrigin,
793
+ },
794
+ mainEntityOfPage: `${runtimeConfig.appOrigin}${page.path}`,
795
+ }, null, 2);
796
+ }
797
+
798
+ function renderSeoPageHtml(page, runtimeConfig = {}) {
799
+ const appOrigin = normalizeText(runtimeConfig.appOrigin) || PRODUCT.homepageUrl;
800
+ const canonicalUrl = `${appOrigin}${page.path}`;
801
+ const relatedCards = page.relatedPages.map((related) => `
802
+ <a class="related-card" href="${escapeHtml(related.path)}">
803
+ <span class="related-label">Related page</span>
804
+ <strong>${escapeHtml(related.title)}</strong>
805
+ </a>`).join('');
806
+ const takeaways = page.takeaways.map((item) => `<li>${escapeHtml(item)}</li>`).join('');
807
+ const sections = page.sections.map((section) => `
808
+ <section class="detail-section">
809
+ <h2>${escapeHtml(section.heading)}</h2>
810
+ ${(section.paragraphs || []).map((paragraph) => `<p>${escapeHtml(paragraph)}</p>`).join('')}
811
+ ${(section.bullets && section.bullets.length) ? `<ul>${section.bullets.map((bullet) => `<li>${escapeHtml(bullet)}</li>`).join('')}</ul>` : ''}
812
+ </section>`).join('');
813
+ const faq = page.faq.map((item) => `
814
+ <details class="faq-item">
815
+ <summary>${escapeHtml(item.question)}</summary>
816
+ <p>${escapeHtml(item.answer)}</p>
817
+ </details>`).join('');
818
+ const proofLinks = page.proofLinks.map((link) => `<a href="${escapeHtml(link.href)}" target="_blank" rel="noopener">${escapeHtml(link.label)}</a>`).join('');
819
+ const faqJsonLd = renderFaqJsonLd(page);
820
+
821
+ return `<!DOCTYPE html>
822
+ <html lang="en">
823
+ <head>
824
+ <meta charset="UTF-8" />
825
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
826
+ <title>${escapeHtml(page.title)}</title>
827
+ <meta name="description" content="${escapeHtml(page.description)}" />
828
+ <meta property="og:title" content="${escapeHtml(page.title)}" />
829
+ <meta property="og:description" content="${escapeHtml(page.description)}" />
830
+ <meta property="og:type" content="article" />
831
+ <meta property="og:url" content="${escapeHtml(canonicalUrl)}" />
832
+ <link rel="canonical" href="${escapeHtml(canonicalUrl)}" />
833
+ <style>
834
+ :root {
835
+ --bg: #0a0a0b;
836
+ --bg-raised: #111113;
837
+ --bg-card: #161618;
838
+ --line: #222225;
839
+ --text: #e8e8ec;
840
+ --muted: #8b8b96;
841
+ --cyan: #22d3ee;
842
+ --green: #4ade80;
843
+ --red: #f87171;
844
+ }
845
+ * { box-sizing: border-box; }
846
+ body {
847
+ margin: 0;
848
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
849
+ background: var(--bg);
850
+ color: var(--text);
851
+ line-height: 1.65;
852
+ }
853
+ a { color: var(--cyan); text-decoration: none; }
854
+ a:hover { text-decoration: underline; }
855
+ .container { max-width: 980px; margin: 0 auto; padding: 0 24px; }
856
+ .topbar {
857
+ position: sticky;
858
+ top: 0;
859
+ z-index: 20;
860
+ backdrop-filter: blur(12px);
861
+ background: rgba(10, 10, 11, 0.88);
862
+ border-bottom: 1px solid var(--line);
863
+ }
864
+ .topbar .container {
865
+ display: flex;
866
+ justify-content: space-between;
867
+ align-items: center;
868
+ padding-top: 14px;
869
+ padding-bottom: 14px;
870
+ }
871
+ .brand {
872
+ font-weight: 700;
873
+ color: var(--text);
874
+ }
875
+ .hero { padding: 72px 0 32px; }
876
+ .eyebrow {
877
+ display: inline-flex;
878
+ align-items: center;
879
+ gap: 8px;
880
+ padding: 6px 12px;
881
+ border-radius: 999px;
882
+ border: 1px solid rgba(34, 211, 238, 0.22);
883
+ background: rgba(34, 211, 238, 0.1);
884
+ color: var(--cyan);
885
+ text-transform: uppercase;
886
+ letter-spacing: 0.08em;
887
+ font-size: 12px;
888
+ font-weight: 700;
889
+ }
890
+ h1 {
891
+ font-size: clamp(34px, 5vw, 56px);
892
+ line-height: 1.06;
893
+ letter-spacing: -0.04em;
894
+ margin: 16px 0;
895
+ max-width: 760px;
896
+ }
897
+ .hero p {
898
+ max-width: 720px;
899
+ color: var(--muted);
900
+ font-size: 18px;
901
+ }
902
+ .signal-row {
903
+ display: flex;
904
+ flex-wrap: wrap;
905
+ gap: 12px;
906
+ margin: 28px 0 0;
907
+ }
908
+ .signal-pill {
909
+ display: inline-flex;
910
+ align-items: center;
911
+ gap: 8px;
912
+ padding: 10px 14px;
913
+ border-radius: 999px;
914
+ border: 1px solid var(--line);
915
+ background: var(--bg-raised);
916
+ font-weight: 600;
917
+ font-size: 14px;
918
+ }
919
+ .signal-pill.up {
920
+ border-color: rgba(74, 222, 128, 0.28);
921
+ color: #b8f7c8;
922
+ background: rgba(74, 222, 128, 0.1);
923
+ }
924
+ .signal-pill.down {
925
+ border-color: rgba(248, 113, 113, 0.28);
926
+ color: #ffc0c0;
927
+ background: rgba(248, 113, 113, 0.1);
928
+ }
929
+ .grid {
930
+ display: grid;
931
+ grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
932
+ gap: 24px;
933
+ padding-bottom: 72px;
934
+ }
935
+ .card, .detail-section, .sidebar-card {
936
+ background: var(--bg-card);
937
+ border: 1px solid var(--line);
938
+ border-radius: 16px;
939
+ }
940
+ .card { padding: 24px; }
941
+ .detail-section { padding: 24px; margin-bottom: 18px; }
942
+ .detail-section h2 { margin: 0 0 12px; font-size: 24px; letter-spacing: -0.03em; }
943
+ .detail-section p { color: var(--muted); }
944
+ .detail-section ul, .card ul { padding-left: 18px; color: var(--muted); }
945
+ .card h2 { margin-top: 0; }
946
+ .sidebar {
947
+ display: flex;
948
+ flex-direction: column;
949
+ gap: 18px;
950
+ }
951
+ .sidebar-card {
952
+ padding: 20px;
953
+ position: sticky;
954
+ top: 84px;
955
+ }
956
+ .proof-links {
957
+ display: flex;
958
+ flex-wrap: wrap;
959
+ gap: 12px;
960
+ margin-top: 16px;
961
+ }
962
+ .cta-button {
963
+ display: inline-flex;
964
+ align-items: center;
965
+ justify-content: center;
966
+ margin-top: 18px;
967
+ padding: 12px 16px;
968
+ border-radius: 10px;
969
+ background: var(--cyan);
970
+ color: #071116;
971
+ font-weight: 700;
972
+ text-decoration: none;
973
+ }
974
+ .faq-item {
975
+ border-top: 1px solid var(--line);
976
+ padding: 14px 0;
977
+ }
978
+ .faq-item summary {
979
+ cursor: pointer;
980
+ font-weight: 600;
981
+ }
982
+ .faq-item p {
983
+ color: var(--muted);
984
+ }
985
+ .related-card {
986
+ display: block;
987
+ padding: 14px;
988
+ border-radius: 12px;
989
+ border: 1px solid var(--line);
990
+ background: var(--bg-raised);
991
+ margin-top: 12px;
992
+ color: var(--text);
993
+ }
994
+ .related-label {
995
+ display: block;
996
+ color: var(--muted);
997
+ font-size: 12px;
998
+ text-transform: uppercase;
999
+ letter-spacing: 0.08em;
1000
+ margin-bottom: 4px;
1001
+ }
1002
+ @media (max-width: 860px) {
1003
+ .grid {
1004
+ grid-template-columns: 1fr;
1005
+ }
1006
+ .sidebar-card {
1007
+ position: static;
1008
+ }
1009
+ }
1010
+ </style>
1011
+ <script type="application/ld+json">
1012
+ ${renderWebPageJsonLd(page, { appOrigin })}
1013
+ </script>
1014
+ ${faqJsonLd ? `<script type="application/ld+json">\n${faqJsonLd}\n </script>` : ''}
1015
+ </head>
1016
+ <body>
1017
+ <div class="topbar">
1018
+ <div class="container">
1019
+ <a class="brand" href="/">👍👎 ThumbGate</a>
1020
+ <a href="${escapeHtml(PRODUCT.verificationUrl)}" target="_blank" rel="noopener">Verification evidence</a>
1021
+ </div>
1022
+ </div>
1023
+
1024
+ <main class="container">
1025
+ <section class="hero">
1026
+ <div class="eyebrow">${escapeHtml(page.pageType)} | ${escapeHtml(page.query)}</div>
1027
+ <h1>${escapeHtml(page.heroTitle)}</h1>
1028
+ <p>${escapeHtml(page.heroSummary)}</p>
1029
+ <div class="signal-row">
1030
+ <div class="signal-pill up">👍 Thumbs up reinforces good behavior</div>
1031
+ <div class="signal-pill down">👎 Thumbs down blocks repeated mistakes</div>
1032
+ </div>
1033
+ </section>
1034
+
1035
+ <section class="grid">
1036
+ <div>
1037
+ <div class="card">
1038
+ <h2>Why this page exists</h2>
1039
+ <ul>${takeaways}</ul>
1040
+ </div>
1041
+ ${sections}
1042
+ <div class="detail-section">
1043
+ <h2>FAQ</h2>
1044
+ ${faq}
1045
+ </div>
1046
+ </div>
1047
+
1048
+ <aside class="sidebar">
1049
+ <div class="sidebar-card">
1050
+ <h2>GSD execution brief</h2>
1051
+ <p>This page was prioritized because it captures high-intent demand around ${escapeHtml(page.query)} and feeds directly into ThumbGate's proof-led conversion path.</p>
1052
+ <p><strong>Opportunity score:</strong> ${page.opportunityScore}</p>
1053
+ <p><strong>Primary persona:</strong> ${escapeHtml(page.persona)}</p>
1054
+ <p><strong>Keyword cluster:</strong> ${escapeHtml(page.keywordCluster.join(', '))}</p>
1055
+ <div class="proof-links">${proofLinks}</div>
1056
+ <a class="cta-button" href="${escapeHtml(page.cta.href)}" target="_blank" rel="noopener">${escapeHtml(page.cta.label)}</a>
1057
+ </div>
1058
+ <div class="sidebar-card">
1059
+ <h2>Related pages</h2>
1060
+ ${relatedCards}
1061
+ </div>
1062
+ </aside>
1063
+ </section>
1064
+ </main>
1065
+ </body>
1066
+ </html>`;
1067
+ }
1068
+
1069
+ const THUMBGATE_SEO_PLAN = buildThumbGateSeoPlan(HIGH_ROI_QUERY_SEEDS);
1070
+ const THUMBGATE_SEO_PAGE_SPECS = THUMBGATE_SEO_PLAN.execute.pages;
1071
+ const THUMBGATE_SEO_SITEMAP_ENTRIES = THUMBGATE_SEO_PAGE_SPECS.map((page) => ({
1072
+ path: page.path,
1073
+ changefreq: page.changefreq,
1074
+ priority: page.priority,
1075
+ }));
1076
+
1077
+ function findSeoPageByPath(pathname) {
1078
+ return THUMBGATE_SEO_PAGE_SPECS.find((page) => page.path === pathname) || null;
1079
+ }
1080
+
1081
+ function parseArgs(argv) {
1082
+ const args = { command: 'full', write: false, input: null, outDir: DEFAULT_OUTPUT_DIR };
1083
+ const tokens = argv.slice(2);
1084
+ for (const token of tokens) {
1085
+ if (token === 'plan' || token === 'full') {
1086
+ args.command = token;
1087
+ continue;
1088
+ }
1089
+ if (token === '--write') {
1090
+ args.write = true;
1091
+ continue;
1092
+ }
1093
+ if (token.startsWith('--input=')) {
1094
+ args.input = token.slice('--input='.length);
1095
+ continue;
1096
+ }
1097
+ if (token.startsWith('--out-dir=')) {
1098
+ args.outDir = path.resolve(token.slice('--out-dir='.length));
1099
+ continue;
1100
+ }
1101
+ }
1102
+ return args;
1103
+ }
1104
+
1105
+ async function main() {
1106
+ const args = parseArgs(process.argv);
1107
+ const rows = args.input ? loadKeywordRows(args.input) : HIGH_ROI_QUERY_SEEDS;
1108
+ const plan = buildThumbGateSeoPlan(rows);
1109
+
1110
+ if (args.write) {
1111
+ const files = writePlanOutputs(plan, args.outDir);
1112
+ console.log(`Wrote SEO GSD outputs to ${args.outDir}`);
1113
+ for (const filePath of Object.values(files)) {
1114
+ console.log(` - ${path.relative(ROOT, filePath)}`);
1115
+ }
1116
+ }
1117
+
1118
+ if (args.command === 'plan' || args.command === 'full') {
1119
+ console.log(renderPlanMarkdown(plan));
1120
+ }
1121
+ }
1122
+
1123
+ if (require.main === module) {
1124
+ main().catch((error) => {
1125
+ console.error(error.message || String(error));
1126
+ process.exit(1);
1127
+ });
1128
+ }
1129
+
1130
+ module.exports = {
1131
+ DEFAULT_OUTPUT_DIR,
1132
+ HIGH_ROI_QUERY_SEEDS,
1133
+ PAGE_BLUEPRINTS,
1134
+ PRODUCT,
1135
+ THUMBGATE_SEO_PLAN,
1136
+ THUMBGATE_SEO_PAGE_SPECS,
1137
+ THUMBGATE_SEO_SITEMAP_ENTRIES,
1138
+ buildThumbGateSeoPlan,
1139
+ classifyIntent,
1140
+ clusterKeywordRows,
1141
+ createPageSpec,
1142
+ findSeoPageByPath,
1143
+ inferPageType,
1144
+ inferPersona,
1145
+ inferPillar,
1146
+ loadKeywordRows,
1147
+ normalizeKeywordRow,
1148
+ parseCsv,
1149
+ renderPlanMarkdown,
1150
+ renderSeoPageHtml,
1151
+ scoreOpportunity,
1152
+ writePlanOutputs,
1153
+ };