thumbgate 1.4.2 → 1.4.4

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 (279) hide show
  1. package/.claude-plugin/README.md +45 -34
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +3 -3
  4. package/.well-known/llms.txt +1 -1
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +26 -2
  7. package/adapters/README.md +4 -1
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/mcp/server-stdio.js +10 -4
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +246 -90
  13. package/config/mcp-allowlists.json +11 -3
  14. package/package.json +184 -21
  15. package/scripts/audit-trail.js +25 -15
  16. package/scripts/auto-wire-hooks.js +127 -0
  17. package/scripts/cli-demo.js +102 -0
  18. package/scripts/cli-schema.js +285 -0
  19. package/scripts/cli-status.js +166 -0
  20. package/scripts/cross-encoder-reranker.js +235 -0
  21. package/scripts/explore-subcommands.js +277 -0
  22. package/scripts/explore.js +569 -0
  23. package/scripts/feedback-loop.js +20 -6
  24. package/scripts/lesson-inference.js +7 -1
  25. package/scripts/lesson-reranker.js +263 -0
  26. package/scripts/lesson-retrieval.js +34 -17
  27. package/scripts/lesson-search.js +69 -0
  28. package/scripts/perplexity-client.js +210 -0
  29. package/scripts/reflector-agent.js +2 -2
  30. package/scripts/statusline-local-stats.js +3 -1
  31. package/scripts/statusline.sh +12 -11
  32. package/src/api/server.js +178 -17
  33. package/src/index.js +3 -0
  34. package/.claude-plugin/bundle/icon.png +0 -0
  35. package/.claude-plugin/bundle/icon.svg +0 -18
  36. package/.claude-plugin/bundle/server/index.js +0 -24
  37. package/adapters/chatgpt/INSTALL.md +0 -138
  38. package/bin/memory.sh +0 -64
  39. package/bin/obsidian-sync.sh +0 -20
  40. package/plugins/amp-skill/INSTALL.md +0 -52
  41. package/plugins/amp-skill/SKILL.md +0 -64
  42. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +0 -22
  43. package/plugins/claude-codex-bridge/.mcp.json +0 -14
  44. package/plugins/claude-codex-bridge/INSTALL.md +0 -43
  45. package/plugins/claude-codex-bridge/README.md +0 -46
  46. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +0 -286
  47. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +0 -24
  48. package/plugins/claude-codex-bridge/skills/result/SKILL.md +0 -22
  49. package/plugins/claude-codex-bridge/skills/review/SKILL.md +0 -28
  50. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +0 -27
  51. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +0 -21
  52. package/plugins/claude-codex-bridge/skills/status/SKILL.md +0 -19
  53. package/plugins/claude-skill/INSTALL.md +0 -55
  54. package/plugins/claude-skill/SKILL.md +0 -46
  55. package/plugins/codex-profile/.codex-plugin/plugin.json +0 -43
  56. package/plugins/codex-profile/.mcp.json +0 -14
  57. package/plugins/codex-profile/AGENTS.md +0 -20
  58. package/plugins/codex-profile/INSTALL.md +0 -89
  59. package/plugins/codex-profile/README.md +0 -61
  60. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +0 -23
  61. package/plugins/cursor-marketplace/CHANGELOG.md +0 -30
  62. package/plugins/cursor-marketplace/LICENSE +0 -21
  63. package/plugins/cursor-marketplace/README.md +0 -124
  64. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +0 -31
  65. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  66. package/plugins/cursor-marketplace/commands/capture-feedback.md +0 -33
  67. package/plugins/cursor-marketplace/commands/check-gates.md +0 -25
  68. package/plugins/cursor-marketplace/commands/show-lessons.md +0 -27
  69. package/plugins/cursor-marketplace/hooks/hooks.json +0 -10
  70. package/plugins/cursor-marketplace/mcp.json +0 -14
  71. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +0 -34
  72. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +0 -30
  73. package/plugins/cursor-marketplace/rules/session-continuity.mdc +0 -28
  74. package/plugins/cursor-marketplace/scripts/gate-check.sh +0 -21
  75. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +0 -48
  76. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +0 -31
  77. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +0 -30
  78. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +0 -33
  79. package/plugins/gemini-extension/INSTALL.md +0 -92
  80. package/plugins/gemini-extension/gemini_prompt.txt +0 -14
  81. package/plugins/gemini-extension/tool_contract.json +0 -45
  82. package/plugins/opencode-profile/INSTALL.md +0 -57
  83. package/public/assets/instagram-card.png +0 -0
  84. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  85. package/public/blog.html +0 -474
  86. package/public/compare/mem0.html +0 -189
  87. package/public/compare/speclock.html +0 -180
  88. package/public/compare.html +0 -310
  89. package/public/dashboard.html +0 -1100
  90. package/public/guide.html +0 -317
  91. package/public/guides/claude-code-prevent-repeated-mistakes.html +0 -161
  92. package/public/guides/codex-cli-guardrails.html +0 -158
  93. package/public/guides/cursor-prevent-repeated-mistakes.html +0 -161
  94. package/public/guides/pre-action-gates.html +0 -162
  95. package/public/guides/stop-repeated-ai-agent-mistakes.html +0 -159
  96. package/public/index.html +0 -1128
  97. package/public/js/buyer-intent.js +0 -252
  98. package/public/learn/agent-harness-pattern.html +0 -180
  99. package/public/learn/ai-agent-persistent-memory.html +0 -203
  100. package/public/learn/learn.css +0 -45
  101. package/public/learn/mcp-pre-action-gates-explained.html +0 -172
  102. package/public/learn/stop-ai-agent-force-push.html +0 -134
  103. package/public/learn/vibe-coding-safety-net.html +0 -142
  104. package/public/learn.html +0 -274
  105. package/public/lessons.html +0 -967
  106. package/public/llm-context.md +0 -140
  107. package/public/pro.html +0 -1087
  108. package/public/vercel.json +0 -8
  109. package/scripts/a2ui-engine.js +0 -73
  110. package/scripts/adk-consolidator.js +0 -274
  111. package/scripts/agent-security-hardening.js +0 -225
  112. package/scripts/ai-search-visibility.js +0 -142
  113. package/scripts/autonomous-sales-agent.js +0 -39
  114. package/scripts/autoresearch-runner.js +0 -216
  115. package/scripts/background-agent-governance.js +0 -229
  116. package/scripts/behavioral-extraction.js +0 -93
  117. package/scripts/budget-enforcer.js +0 -173
  118. package/scripts/budget-guard.js +0 -173
  119. package/scripts/build-claude-mcpb.js +0 -255
  120. package/scripts/build-codex-plugin.js +0 -152
  121. package/scripts/capture-railway-diagnostics.sh +0 -97
  122. package/scripts/changeset-check.js +0 -372
  123. package/scripts/check-congruence.js +0 -443
  124. package/scripts/computer-use-firewall.js +0 -280
  125. package/scripts/content-engine/linkedin-content-generator.js +0 -154
  126. package/scripts/content-engine/output/linkedin-memento-validation.md +0 -17
  127. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +0 -175
  128. package/scripts/content-engine/reddit-thread-finder.js +0 -154
  129. package/scripts/context-engine.js +0 -710
  130. package/scripts/daily-digest.js +0 -11
  131. package/scripts/data-governance.js +0 -173
  132. package/scripts/deploy-gcp.sh +0 -44
  133. package/scripts/deploy-policy.js +0 -249
  134. package/scripts/disagreement-mining.js +0 -315
  135. package/scripts/dpo-optimizer.js +0 -206
  136. package/scripts/ensure-repo-bootstrap.js +0 -130
  137. package/scripts/ephemeral-agent-store.js +0 -212
  138. package/scripts/eval-harness.js +0 -56
  139. package/scripts/export-kto-pairs.js +0 -309
  140. package/scripts/export-training.js +0 -446
  141. package/scripts/feedback-fallback.js +0 -111
  142. package/scripts/feedback-inbox-read.js +0 -162
  143. package/scripts/feedback-root-consolidator.js +0 -233
  144. package/scripts/feedback-to-memory.js +0 -185
  145. package/scripts/gate-satisfy.js +0 -42
  146. package/scripts/generate-paperbanana-diagrams.sh +0 -99
  147. package/scripts/generate-pretool-hook.sh +0 -40
  148. package/scripts/github-about.js +0 -430
  149. package/scripts/github-outreach.js +0 -65
  150. package/scripts/gtm-revenue-loop.js +0 -535
  151. package/scripts/hallucination-detector.js +0 -226
  152. package/scripts/hf-papers.js +0 -317
  153. package/scripts/hook-auto-capture.sh +0 -100
  154. package/scripts/hook-stop-pr-thread-check.sh +0 -68
  155. package/scripts/hook-stop-self-score.sh +0 -51
  156. package/scripts/hook-stop-verify-deploy.sh +0 -31
  157. package/scripts/hook-verify-before-done.sh +0 -20
  158. package/scripts/managed-dpo-export.js +0 -91
  159. package/scripts/markdown-escape.js +0 -12
  160. package/scripts/marketing-experiment.js +0 -657
  161. package/scripts/memalign-recall.js +0 -111
  162. package/scripts/memory-migration.js +0 -296
  163. package/scripts/meta-policy.js +0 -190
  164. package/scripts/metered-billing.js +0 -16
  165. package/scripts/model-tier-router.js +0 -310
  166. package/scripts/money-watcher.js +0 -218
  167. package/scripts/multi-hop-recall.js +0 -240
  168. package/scripts/per-step-scoring.js +0 -163
  169. package/scripts/perplexity-marketing.js +0 -466
  170. package/scripts/pii-scanner.js +0 -153
  171. package/scripts/plan-gate.js +0 -154
  172. package/scripts/post-everywhere.js +0 -341
  173. package/scripts/post-to-x-retry.sh +0 -22
  174. package/scripts/post-to-x.js +0 -369
  175. package/scripts/pr-manager.js +0 -421
  176. package/scripts/principle-extractor.js +0 -162
  177. package/scripts/pro-features.js +0 -41
  178. package/scripts/prompt-dlp.js +0 -222
  179. package/scripts/prove-adapters.js +0 -860
  180. package/scripts/prove-attribution.js +0 -361
  181. package/scripts/prove-automation.js +0 -651
  182. package/scripts/prove-autoresearch.js +0 -304
  183. package/scripts/prove-claim-verification.js +0 -277
  184. package/scripts/prove-cloudflare-sandbox.js +0 -161
  185. package/scripts/prove-data-pipeline.js +0 -408
  186. package/scripts/prove-data-quality.js +0 -227
  187. package/scripts/prove-evolution.js +0 -352
  188. package/scripts/prove-harnesses.js +0 -287
  189. package/scripts/prove-intelligence.js +0 -257
  190. package/scripts/prove-lancedb.js +0 -425
  191. package/scripts/prove-local-intelligence.js +0 -340
  192. package/scripts/prove-loop-closure.js +0 -263
  193. package/scripts/prove-packaged-runtime.js +0 -326
  194. package/scripts/prove-predictive-insights.js +0 -355
  195. package/scripts/prove-runtime.js +0 -363
  196. package/scripts/prove-seo-gsd.js +0 -234
  197. package/scripts/prove-settings.js +0 -279
  198. package/scripts/prove-subway-upgrades.js +0 -277
  199. package/scripts/prove-tessl.js +0 -229
  200. package/scripts/prove-training-export.js +0 -325
  201. package/scripts/prove-workflow-contract.js +0 -112
  202. package/scripts/prove-xmemory.js +0 -332
  203. package/scripts/publish-decision.js +0 -159
  204. package/scripts/ralph-loop.js +0 -376
  205. package/scripts/ralph-mode-ci.js +0 -331
  206. package/scripts/reddit-dm-outreach.js +0 -192
  207. package/scripts/reddit-monitor-cron.sh +0 -26
  208. package/scripts/reminder-engine.js +0 -132
  209. package/scripts/revenue-status.js +0 -472
  210. package/scripts/rotate-stripe-webhook-secret.js +0 -314
  211. package/scripts/schedule-manager.js +0 -249
  212. package/scripts/self-healing-check.js +0 -193
  213. package/scripts/shieldcortex-memory-firewall-runner.mjs +0 -53
  214. package/scripts/skill-exporter.js +0 -260
  215. package/scripts/skill-materializer.js +0 -134
  216. package/scripts/skill-packs.js +0 -136
  217. package/scripts/skill-proposer.js +0 -99
  218. package/scripts/skill-quality-tracker.js +0 -282
  219. package/scripts/slow-loop.js +0 -72
  220. package/scripts/social-analytics/db/analytics.sqlite +0 -0
  221. package/scripts/social-analytics/db/schema.sql +0 -32
  222. package/scripts/social-analytics/digest.js +0 -256
  223. package/scripts/social-analytics/engagement-audit.js +0 -185
  224. package/scripts/social-analytics/generate-instagram-card.js +0 -97
  225. package/scripts/social-analytics/instagram-thumbgate-post.js +0 -111
  226. package/scripts/social-analytics/install-growth-automation.js +0 -114
  227. package/scripts/social-analytics/load-env.js +0 -77
  228. package/scripts/social-analytics/mcp-server.js +0 -289
  229. package/scripts/social-analytics/normalizer.js +0 -580
  230. package/scripts/social-analytics/notify.js +0 -162
  231. package/scripts/social-analytics/poll-all.js +0 -107
  232. package/scripts/social-analytics/pollers/github.js +0 -195
  233. package/scripts/social-analytics/pollers/instagram.js +0 -253
  234. package/scripts/social-analytics/pollers/linkedin.js +0 -340
  235. package/scripts/social-analytics/pollers/plausible.js +0 -245
  236. package/scripts/social-analytics/pollers/reddit.js +0 -306
  237. package/scripts/social-analytics/pollers/threads.js +0 -233
  238. package/scripts/social-analytics/pollers/tiktok.js +0 -203
  239. package/scripts/social-analytics/pollers/x.js +0 -227
  240. package/scripts/social-analytics/pollers/youtube.js +0 -304
  241. package/scripts/social-analytics/pollers/zernio.js +0 -183
  242. package/scripts/social-analytics/publish-instagram-thumbgate.js +0 -104
  243. package/scripts/social-analytics/publish-thumbgate-launch.js +0 -322
  244. package/scripts/social-analytics/publishers/devto.js +0 -122
  245. package/scripts/social-analytics/publishers/instagram.js +0 -317
  246. package/scripts/social-analytics/publishers/linkedin.js +0 -294
  247. package/scripts/social-analytics/publishers/reddit.js +0 -385
  248. package/scripts/social-analytics/publishers/threads.js +0 -275
  249. package/scripts/social-analytics/publishers/tiktok.js +0 -217
  250. package/scripts/social-analytics/publishers/x.js +0 -259
  251. package/scripts/social-analytics/publishers/youtube.js +0 -223
  252. package/scripts/social-analytics/publishers/zernio.js +0 -539
  253. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +0 -165
  254. package/scripts/social-analytics/run-digest.js +0 -34
  255. package/scripts/social-analytics/schedule-thumbgate-campaign.js +0 -275
  256. package/scripts/social-analytics/store.js +0 -455
  257. package/scripts/social-analytics/sync-launch-assets.js +0 -185
  258. package/scripts/social-analytics/utm.js +0 -143
  259. package/scripts/social-pipeline.js +0 -2626
  260. package/scripts/social-post-hourly.js +0 -228
  261. package/scripts/social-quality-gate.js +0 -134
  262. package/scripts/social-reply-monitor.js +0 -592
  263. package/scripts/status-dashboard.js +0 -155
  264. package/scripts/stripe-live-status.js +0 -115
  265. package/scripts/subagent-profiles.js +0 -79
  266. package/scripts/sync-branch-protection.js +0 -340
  267. package/scripts/sync-gh-secrets-from-env.sh +0 -70
  268. package/scripts/sync-github-about.js +0 -55
  269. package/scripts/sync-version.js +0 -479
  270. package/scripts/synthetic-dpo.js +0 -234
  271. package/scripts/tessl-export.js +0 -369
  272. package/scripts/test-coverage.js +0 -128
  273. package/scripts/thumbgate_session_start.sh +0 -32
  274. package/scripts/train_from_feedback.py +0 -929
  275. package/scripts/validate-feedback.js +0 -581
  276. package/scripts/verify-obsidian-setup.sh +0 -269
  277. package/scripts/verify-run.js +0 -269
  278. package/scripts/weekly-auto-post.js +0 -124
  279. package/scripts/x-autonomous-marketing.js +0 -139
@@ -1,136 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { registerPreventionRules } = require('./contextfs');
6
- const SKILL_PACKS_DIR = path.join(__dirname, '..', 'config', 'skill-packs');
7
- const BUILTIN_PACKS = {
8
- 'stripe-integration': { name: 'stripe-integration', description: 'Stripe API best practices', triggers: ['stripe', 'payment', 'checkout', 'subscription', 'webhook signature'], rules: ['ALWAYS use idempotency keys on PaymentIntent creation to prevent duplicate charges.', 'NEVER log or store raw card numbers — use Stripe tokens or PaymentMethod IDs.', 'ALWAYS verify webhook signatures with stripe.webhooks.constructEvent() before processing.', 'Use Checkout Sessions instead of raw PaymentIntents for new integrations.', 'ALWAYS handle payment_intent.succeeded AND payment_intent.payment_failed webhooks.'], packTemplate: { namespaces: ['memoryError', 'memoryLearning', 'rules'], maxItems: 8, maxChars: 6000, queryPrefix: 'stripe payment checkout webhook idempotency' } },
9
- 'railway-deploy': { name: 'railway-deploy', description: 'Railway deployment best practices', triggers: ['railway', 'deploy', 'dockerfile', 'health check'], rules: ['ALWAYS verify /health endpoint returns new version after deploy.', 'NEVER say "deployed" without curling the health endpoint and showing version match.', 'ALWAYS check Railway build logs for warnings even when deploy succeeds.', 'Use RAILWAY_VOLUME_MOUNT_PATH for persistent data.', 'ALWAYS wait 2-5 minutes after merge before verifying.'], packTemplate: { namespaces: ['memoryError', 'memoryLearning', 'rules'], maxItems: 8, maxChars: 6000, queryPrefix: 'railway deploy health version dockerfile' } },
10
- 'database-migration': { name: 'database-migration', description: 'Database migration best practices', triggers: ['migration', 'prisma', 'sqlite', 'schema', 'alter table'], rules: ['ALWAYS back up the database before running destructive migrations.', 'NEVER drop columns in production without verifying no code references them.', 'ALWAYS run migrations against a test database first.', 'Use reversible migrations — every up() should have a corresponding down().', 'ALWAYS check for pending migrations before deploying new code.'], packTemplate: { namespaces: ['memoryError', 'rules'], maxItems: 6, maxChars: 5000, queryPrefix: 'migration database schema prisma sqlite' } },
11
- };
12
- const registry = new Map(); for (const [id, p] of Object.entries(BUILTIN_PACKS)) registry.set(id, p);
13
- function ensurePacksDir() { if (!fs.existsSync(SKILL_PACKS_DIR)) fs.mkdirSync(SKILL_PACKS_DIR, { recursive: true }); }
14
- function registerSkillPack(pack) { if (!pack.name) throw new Error('Skill pack requires a name'); if (!Array.isArray(pack.rules) || pack.rules.length === 0) throw new Error('Skill pack requires at least one rule'); const n = { name: pack.name, description: pack.description || '', triggers: Array.isArray(pack.triggers) ? pack.triggers : [], rules: pack.rules, packTemplate: pack.packTemplate || null, registeredAt: new Date().toISOString() }; registry.set(n.name, n); ensurePacksDir(); fs.writeFileSync(path.join(SKILL_PACKS_DIR, `${n.name}.json`), JSON.stringify(n, null, 2) + '\n'); return n; }
15
- function loadSkillPacksFromDisk() { ensurePacksDir(); for (const f of fs.readdirSync(SKILL_PACKS_DIR).filter((x) => x.endsWith('.json'))) { try { const p = JSON.parse(fs.readFileSync(path.join(SKILL_PACKS_DIR, f), 'utf-8')); if (p.name) registry.set(p.name, p); } catch { /* skip */ } } }
16
- function listSkillPacks() { loadSkillPacksFromDisk(); return Array.from(registry.values()).map((p) => ({ name: p.name, description: p.description, triggers: p.triggers, ruleCount: p.rules.length, hasPackTemplate: !!p.packTemplate })); }
17
- function getSkillPack(name) { loadSkillPacksFromDisk(); return registry.get(name) || null; }
18
- function matchSkillPacks(query) { const tokens = String(query || '').toLowerCase().split(/\s+/).filter(Boolean); if (tokens.length === 0) return []; loadSkillPacksFromDisk(); const scored = []; for (const pack of registry.values()) { let score = 0; for (const trigger of pack.triggers) { for (const t of trigger.toLowerCase().split(/\s+/)) { if (tokens.some((qt) => qt.includes(t) || t.includes(qt))) score += 1; } } if (score > 0) scored.push({ pack, score }); } return scored.sort((a, b) => b.score - a.score).map((s) => s.pack); }
19
- function installSkillPackRules(name) { const pack = getSkillPack(name); if (!pack) throw new Error(`Skill pack not found: "${name}"`); return registerPreventionRules([`# Skill Pack: ${pack.name}`, '', pack.description || '', '', ...pack.rules.map((r, i) => `${i + 1}. ${r}`)].join('\n'), { skillPack: pack.name }); }
20
- // ---------------------------------------------------------------------------
21
- // L3 Resource Loading (ADK progressive disclosure)
22
- // ---------------------------------------------------------------------------
23
-
24
- const RESOURCES_DIR_NAME = 'references';
25
-
26
- /**
27
- * Load an L3 resource file for a skill pack.
28
- * Resources live in config/skill-packs/{pack-name}/references/{filename}.
29
- */
30
- function loadSkillResource(packName, resourceName) {
31
- const resDir = path.join(SKILL_PACKS_DIR, packName, RESOURCES_DIR_NAME);
32
- const resPath = path.join(resDir, resourceName);
33
- if (!fs.existsSync(resPath)) return null;
34
- return { name: resourceName, path: resPath, content: fs.readFileSync(resPath, 'utf-8'), sizeBytes: fs.statSync(resPath).size };
35
- }
36
-
37
- /**
38
- * List available L3 resources for a skill pack.
39
- */
40
- function listSkillResources(packName) {
41
- const resDir = path.join(SKILL_PACKS_DIR, packName, RESOURCES_DIR_NAME);
42
- if (!fs.existsSync(resDir)) return [];
43
- return fs.readdirSync(resDir).filter((f) => !f.startsWith('.')).map((f) => {
44
- const fp = path.join(resDir, f);
45
- return { name: f, sizeBytes: fs.statSync(fp).size };
46
- });
47
- }
48
-
49
- /**
50
- * Add an L3 resource file to a skill pack.
51
- */
52
- function addSkillResource(packName, resourceName, content) {
53
- const resDir = path.join(SKILL_PACKS_DIR, packName, RESOURCES_DIR_NAME);
54
- ensurePacksDir();
55
- if (!fs.existsSync(resDir)) fs.mkdirSync(resDir, { recursive: true });
56
- const resPath = path.join(resDir, resourceName);
57
- fs.writeFileSync(resPath, content);
58
- return { name: resourceName, path: resPath, sizeBytes: Buffer.byteLength(content) };
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // Skill Factory — agent-driven skill generation
63
- // ---------------------------------------------------------------------------
64
-
65
- /**
66
- * Auto-generate a skill pack from recurring failure patterns.
67
- * Uses distilled lessons to propose rules for a new domain.
68
- *
69
- * @param {Object} opts
70
- * @param {string} opts.domain - Domain name (e.g., 'graphql-api')
71
- * @param {Array} opts.lessons - Array of lesson strings from history distiller
72
- * @param {string} [opts.description] - Pack description
73
- * @param {Array} [opts.triggers] - Trigger keywords
74
- * @returns {Object} The created skill pack
75
- */
76
- function generateSkillPack({ domain, lessons, description, triggers } = {}) {
77
- if (!domain) throw new Error('Skill factory requires a domain name');
78
- if (!Array.isArray(lessons) || lessons.length === 0) throw new Error('Skill factory requires at least one lesson');
79
-
80
- // Convert lessons into NEVER/ALWAYS rules
81
- const rules = lessons.map((lesson) => {
82
- const l = String(lesson).trim();
83
- if (/^(NEVER|ALWAYS|DO NOT|MUST)/i.test(l)) return l;
84
- if (/fail|error|broke|wrong|bug|crash/i.test(l)) return `NEVER ${l.replace(/^(avoid|don'?t|stop)\s*/i, '').trim()}`;
85
- return `ALWAYS ${l.replace(/^(repeat|keep|continue)\s*/i, '').trim()}`;
86
- });
87
-
88
- // Infer triggers from domain + lesson content
89
- const inferredTriggers = triggers || [domain, ...domain.split('-').filter((t) => t.length > 2)];
90
-
91
- return registerSkillPack({
92
- name: domain,
93
- description: description || `Auto-generated skill pack for ${domain} from ${lessons.length} lessons`,
94
- triggers: inferredTriggers,
95
- rules,
96
- packTemplate: { namespaces: ['memoryError', 'memoryLearning', 'rules'], maxItems: 8, maxChars: 6000, queryPrefix: inferredTriggers.join(' ') },
97
- });
98
- }
99
-
100
- // ---------------------------------------------------------------------------
101
- // Token-Efficient Progressive Disclosure Metrics
102
- // ---------------------------------------------------------------------------
103
-
104
- /**
105
- * Measure token cost of each disclosure level for a skill pack.
106
- * Helps agents decide which packs to load.
107
- */
108
- function measureSkillTokens(packName) {
109
- const pack = getSkillPack(packName);
110
- if (!pack) return null;
111
-
112
- // L1: metadata only (~name + description + triggers)
113
- const l1Text = `${pack.name}: ${pack.description} [${(pack.triggers || []).join(', ')}]`;
114
- const l1Chars = l1Text.length;
115
-
116
- // L2: full rules
117
- const l2Text = pack.rules.join('\n');
118
- const l2Chars = l2Text.length;
119
-
120
- // L3: resources
121
- const resources = listSkillResources(packName);
122
- const l3Chars = resources.reduce((sum, r) => sum + r.sizeBytes, 0);
123
-
124
- const totalChars = l1Chars + l2Chars + l3Chars;
125
-
126
- return {
127
- packName,
128
- l1: { chars: l1Chars, estimatedTokens: Math.ceil(l1Chars / 4) },
129
- l2: { chars: l2Chars, estimatedTokens: Math.ceil(l2Chars / 4), ruleCount: pack.rules.length },
130
- l3: { chars: l3Chars, estimatedTokens: Math.ceil(l3Chars / 4), resourceCount: resources.length },
131
- total: { chars: totalChars, estimatedTokens: Math.ceil(totalChars / 4) },
132
- disclosureSavings: totalChars > 0 ? Math.round((1 - l1Chars / totalChars) * 100) : 0,
133
- };
134
- }
135
-
136
- module.exports = { BUILTIN_PACKS, registerSkillPack, listSkillPacks, getSkillPack, matchSkillPacks, installSkillPackRules, SKILL_PACKS_DIR, loadSkillResource, listSkillResources, addSkillResource, generateSkillPack, measureSkillTokens };
@@ -1,99 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Skill Proposer (EvoSkill Phase 1)
4
- *
5
- * Analyzes recurring failure patterns in memory-log.jsonl.
6
- * Diagnoses the root cause using 'Reasoning' traces and proposes
7
- * a new functional skill (tool) to solve the capability gap.
8
- */
9
-
10
- const fs = require('fs');
11
- const path = require('path');
12
- const {
13
- parseFeedbackFile,
14
- clusterByTags,
15
- extractTags,
16
- discoverFeedbackDir
17
- } = require('./skill-generator');
18
-
19
- function proposeSkills(options = {}) {
20
- const feedbackDir = options.feedbackDir || discoverFeedbackDir();
21
- const logPath = path.join(feedbackDir, 'memory-log.jsonl');
22
- const proposalsDir = path.join(feedbackDir, 'skill-proposals');
23
-
24
- const memories = parseFeedbackFile(logPath);
25
- const mistakes = memories.filter(m => m.category === 'error' || m.title.startsWith('MISTAKE:'));
26
-
27
- if (mistakes.length === 0) {
28
- console.log('No mistakes found in memory log.');
29
- return [];
30
- }
31
-
32
- // Cluster by tags (EvoSkill refinement)
33
- const clusters = clusterByTags(mistakes, 2);
34
- const proposals = [];
35
-
36
- for (const [tagKey, cluster] of clusters) {
37
- if (cluster.entries.length < 2) continue; // Lower threshold for autonomous discovery
38
-
39
- console.log(`Analyzing cluster: [${tagKey}] (${cluster.entries.length} evidences)`);
40
-
41
- // Extract root cause from reasoning traces
42
- const reasoningTraces = cluster.entries
43
- .map(e => {
44
- const match = e.content.match(/Reasoning: (.*)/);
45
- return match ? match[1] : null;
46
- })
47
- .filter(Boolean);
48
-
49
- const commonProblem = cluster.entries[0].title.replace('MISTAKE: ', '');
50
- const tags = cluster.tags;
51
-
52
- const proposal = {
53
- id: `prop_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
54
- status: 'pending',
55
- problem: commonProblem,
56
- diagnosis: reasoningTraces.length > 0 ? reasoningTraces[0] : 'Repeated execution failure in this domain.',
57
- suggestedSkill: {
58
- name: `solve-${tags[0]}-${tags[1] || 'logic'}`.toLowerCase().replace(/[^a-z-]/g, ''),
59
- description: `Automated skill to handle ${tags.join(', ')} patterns efficiently.`,
60
- tags,
61
- // Propose a generic tool structure that the Materializer can flesh out
62
- toolSpec: {
63
- name: `handle_${tags[0].replace(/-/g, '_')}`,
64
- description: `Fixes ${commonProblem}`,
65
- parameters: {
66
- type: 'object',
67
- properties: {
68
- context: { type: 'string', description: 'The current task context' }
69
- }
70
- }
71
- }
72
- },
73
- evidenceIds: cluster.entries.map(e => e.id),
74
- timestamp: new Date().toISOString()
75
- };
76
-
77
- proposals.push(proposal);
78
-
79
- if (!fs.existsSync(proposalsDir)) fs.mkdirSync(proposalsDir, { recursive: true });
80
- fs.writeFileSync(
81
- path.join(proposalsDir, `${proposal.suggestedSkill.name}.json`),
82
- JSON.stringify(proposal, null, 2)
83
- );
84
- }
85
-
86
- return proposals;
87
- }
88
-
89
- if (require.main === module) {
90
- const props = proposeSkills();
91
- if (props && props.length > 0) {
92
- const feedbackDir = discoverFeedbackDir();
93
- const proposalsDir = path.join(feedbackDir, 'skill-proposals');
94
- console.log(`\nGenerated ${props.length} skill proposals in ${proposalsDir}`);
95
- props.forEach(p => console.log(` - ${p.suggestedSkill.name}: ${p.problem}`));
96
- }
97
- }
98
-
99
- module.exports = { proposeSkills };
@@ -1,282 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Skill Quality Tracker
4
- *
5
- * Correlates tool call metrics to feedback signals by timestamp proximity.
6
- * After a sequence of tool calls and feedback captures, produces a per-skill
7
- * quality score derived from timestamp-proximity correlation.
8
- *
9
- * Ported from Subway_RN_Demo/.claude/scripts/feedback/skill-quality-tracker.js
10
- * PATH: PROJECT_ROOT = path.join(__dirname, '..') — 1 level up from scripts/
11
- */
12
-
13
- 'use strict';
14
-
15
- const fs = require('fs');
16
- const readline = require('readline');
17
- const path = require('path');
18
- const { resolveFeedbackDir } = require('./feedback-paths');
19
-
20
- const METRICS_PATH = process.env.METRICS_PATH
21
- || path.join(resolveFeedbackDir(), 'tool-metrics.jsonl');
22
-
23
- const FEEDBACK_PATH = process.env.FEEDBACK_PATH
24
- || path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
25
-
26
- // Correlation window: feedback within 60 seconds of a tool call is considered correlated
27
- const CORRELATION_WINDOW_MS = 60_000;
28
-
29
- /**
30
- * Safely parse a single JSON line.
31
- *
32
- * @param {string} line
33
- * @returns {object|null}
34
- */
35
- function parseLine(line) {
36
- try {
37
- return JSON.parse(line);
38
- } catch {
39
- return null;
40
- }
41
- }
42
-
43
- /**
44
- * Load feedback entries from JSONL file.
45
- * Each entry needs: timestamp, feedback (signal).
46
- *
47
- * @param {string} filePath
48
- * @returns {Promise<Array<{ ts: number, feedback: string, tool: string|null }>>}
49
- */
50
- async function loadFeedback(filePath) {
51
- const entries = [];
52
- if (!fs.existsSync(filePath)) return entries;
53
-
54
- const rl = readline.createInterface({
55
- input: fs.createReadStream(filePath),
56
- crlfDelay: Infinity,
57
- });
58
-
59
- for await (const line of rl) {
60
- const obj = parseLine(line);
61
- if (obj && obj.timestamp) {
62
- // Support both 'feedback' (Subway) and 'signal' (ThumbGate) field names
63
- const feedbackVal = obj.feedback || obj.signal;
64
- if (feedbackVal) {
65
- // Normalize to 'positive'/'negative' regardless of source schema
66
- let normalized = feedbackVal;
67
- if (feedbackVal === 'up') normalized = 'positive';
68
- else if (feedbackVal === 'down') normalized = 'negative';
69
-
70
- entries.push({
71
- ts: new Date(obj.timestamp).getTime(),
72
- feedback: normalized,
73
- tool: obj.tool_name || null,
74
- });
75
- }
76
- }
77
- }
78
-
79
- entries.sort((a, b) => a.ts - b.ts);
80
- return entries;
81
- }
82
-
83
- /**
84
- * Find correlated feedback for a tool call by timestamp proximity.
85
- *
86
- * Searches feedback entries within CORRELATION_WINDOW_MS of the metric timestamp.
87
- * If the feedback entry has a tool_name, it must match the metric's tool name.
88
- *
89
- * @param {number} metricTs - Timestamp of the tool call (ms)
90
- * @param {string} metricTool - Tool name
91
- * @param {Array<{ ts: number, feedback: string, tool: string|null }>} feedbackEntries
92
- * @returns {string|null} 'positive', 'negative', or null if no correlation found
93
- */
94
- function correlateFeedback(metricTs, metricTool, feedbackEntries) {
95
- for (const fb of feedbackEntries) {
96
- if (Math.abs(fb.ts - metricTs) <= CORRELATION_WINDOW_MS) {
97
- // If feedback has a tool name, it must match; otherwise correlate by time alone
98
- if (!fb.tool || fb.tool === metricTool) {
99
- return fb.feedback;
100
- }
101
- }
102
- }
103
- return null;
104
- }
105
-
106
- /**
107
- * Process tool metrics JSONL and correlate with feedback.
108
- *
109
- * @param {string} metricsPath
110
- * @param {Array<{ ts: number, feedback: string, tool: string|null }>} feedbackEntries
111
- * @returns {Promise<{ totalToolUses: number, breakdown: object }>}
112
- */
113
- async function processMetrics(metricsPath, feedbackEntries) {
114
- const breakdown = {};
115
- let totalToolUses = 0;
116
-
117
- if (!fs.existsSync(metricsPath)) return { totalToolUses, breakdown };
118
-
119
- const rl = readline.createInterface({
120
- input: fs.createReadStream(metricsPath),
121
- crlfDelay: Infinity,
122
- });
123
-
124
- for await (const line of rl) {
125
- const obj = parseLine(line);
126
- if (!obj || !obj.tool_name) continue;
127
-
128
- totalToolUses++;
129
- const name = obj.tool_name;
130
-
131
- if (!breakdown[name]) {
132
- breakdown[name] = { uses: 0, correlatedPositive: 0, correlatedNegative: 0 };
133
- }
134
-
135
- breakdown[name].uses++;
136
-
137
- const ts = new Date(obj.timestamp).getTime();
138
- if (!isNaN(ts)) {
139
- const signal = correlateFeedback(ts, name, feedbackEntries);
140
- if (signal === 'positive') breakdown[name].correlatedPositive++;
141
- else if (signal === 'negative') breakdown[name].correlatedNegative++;
142
- }
143
- }
144
-
145
- return { totalToolUses, breakdown };
146
- }
147
-
148
- /**
149
- * Compute per-tool success rates from correlation counts.
150
- * Mutates the breakdown object in place.
151
- *
152
- * @param {object} breakdown - { toolName: { uses, correlatedPositive, correlatedNegative } }
153
- */
154
- function computeSuccessRates(breakdown) {
155
- for (const tool of Object.values(breakdown)) {
156
- const correlated = tool.correlatedPositive + tool.correlatedNegative;
157
- tool.successRate = correlated > 0
158
- ? +(tool.correlatedPositive / correlated).toFixed(4)
159
- : null;
160
- }
161
- }
162
-
163
- /**
164
- * Return top-performing tools sorted by success rate.
165
- *
166
- * @param {object} breakdown
167
- * @param {number} [min=10] - Minimum uses threshold
168
- * @param {number} [limit=5] - Maximum entries to return
169
- * @returns {Array<{ tool: string, successRate: number, uses: number }>}
170
- */
171
- function topPerformers(breakdown, min = 10, limit = 5) {
172
- return Object.entries(breakdown)
173
- .filter(([, v]) => v.uses >= min && v.successRate !== null)
174
- .sort((a, b) => b[1].successRate - a[1].successRate || b[1].uses - a[1].uses)
175
- .slice(0, limit)
176
- .map(([name, v]) => ({ tool: name, successRate: v.successRate, uses: v.uses }));
177
- }
178
-
179
- /**
180
- * Return tools with high negative correlation (potential trouble spots).
181
- * Threshold: >30% negative rate among correlated feedback.
182
- *
183
- * @param {object} breakdown
184
- * @returns {Array<{ tool: string, negativeRate: number, uses: number }>}
185
- */
186
- function troubleSpots(breakdown) {
187
- return Object.entries(breakdown)
188
- .filter(([, v]) => {
189
- const total = v.correlatedPositive + v.correlatedNegative;
190
- return total > 0 && v.correlatedNegative / total > 0.3;
191
- })
192
- .map(([name, v]) => {
193
- const total = v.correlatedPositive + v.correlatedNegative;
194
- return {
195
- tool: name,
196
- negativeRate: +(v.correlatedNegative / total).toFixed(4),
197
- uses: v.uses,
198
- };
199
- })
200
- .sort((a, b) => b.negativeRate - a.negativeRate);
201
- }
202
-
203
- /**
204
- * Generate actionable recommendations from top performers and trouble spots.
205
- *
206
- * @param {Array} top - topPerformers result
207
- * @param {Array} trouble - troubleSpots result
208
- * @param {object} breakdown - full breakdown
209
- * @returns {string[]}
210
- */
211
- function generateRecommendations(top, trouble, breakdown) {
212
- const recs = [];
213
-
214
- for (const t of trouble) {
215
- recs.push(
216
- `Investigate "${t.tool}" — ${(t.negativeRate * 100).toFixed(1)}% negative correlation across ${t.uses} uses.`
217
- );
218
- }
219
-
220
- if (top.length > 0) {
221
- recs.push(
222
- `"${top[0].tool}" is the top performer (${(top[0].successRate * 100).toFixed(1)}% success). Consider expanding its usage patterns.`
223
- );
224
- }
225
-
226
- const uncorrelated = Object.entries(breakdown).filter(
227
- ([, v]) => v.uses >= 10 && v.successRate === null
228
- );
229
- if (uncorrelated.length > 0) {
230
- recs.push(
231
- `${uncorrelated.length} tool(s) with 10+ uses have no correlated feedback — consider adding coverage.`
232
- );
233
- }
234
-
235
- if (recs.length === 0) recs.push('No actionable recommendations at this time.');
236
- return recs;
237
- }
238
-
239
- /**
240
- * Main entry point: load data, correlate, produce report.
241
- *
242
- * @returns {Promise<object>} Full skill quality report
243
- */
244
- async function run() {
245
- const feedbackEntries = await loadFeedback(FEEDBACK_PATH);
246
- const { totalToolUses, breakdown } = await processMetrics(METRICS_PATH, feedbackEntries);
247
-
248
- computeSuccessRates(breakdown);
249
-
250
- const top = topPerformers(breakdown);
251
- const trouble = troubleSpots(breakdown);
252
- const recommendations = generateRecommendations(top, trouble, breakdown);
253
-
254
- const report = {
255
- generatedAt: new Date().toISOString(),
256
- totalToolUses,
257
- toolBreakdown: breakdown,
258
- topPerformers: top,
259
- troubleSpots: trouble,
260
- recommendations,
261
- };
262
-
263
- console.log(JSON.stringify(report, null, 2));
264
- return report;
265
- }
266
-
267
- if (require.main === module) {
268
- run().catch(() => {}).finally(() => process.exit(0));
269
- }
270
-
271
- module.exports = {
272
- parseLine,
273
- correlateFeedback,
274
- computeSuccessRates,
275
- topPerformers,
276
- troubleSpots,
277
- generateRecommendations,
278
- loadFeedback,
279
- processMetrics,
280
- run,
281
- CORRELATION_WINDOW_MS,
282
- };
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { createSchedule } = require('./schedule-manager');
7
- const { resolveFeedbackDir } = require('./feedback-paths');
8
-
9
- const IDLE_THRESHOLD_MINUTES = 30;
10
- const SLOW_LOOP_STATE_FILE = 'slow-loop-state.json';
11
-
12
- function getStatePath() {
13
- return path.join(resolveFeedbackDir(), SLOW_LOOP_STATE_FILE);
14
- }
15
-
16
- function loadState() {
17
- const p = getStatePath();
18
- if (!fs.existsSync(p)) return { lastExportAt: null, exportCount: 0, lastIdleCheckAt: null, totalPairsExported: 0 };
19
- try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return { lastExportAt: null, exportCount: 0, lastIdleCheckAt: null, totalPairsExported: 0 }; }
20
- }
21
-
22
- function saveState(state) {
23
- const p = getStatePath();
24
- const dir = path.dirname(p);
25
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
26
- fs.writeFileSync(p, JSON.stringify(state, null, 2) + '\n');
27
- }
28
-
29
- function isIdle({ thresholdMinutes = IDLE_THRESHOLD_MINUTES } = {}) {
30
- const feedbackDir = resolveFeedbackDir();
31
- const logPath = path.join(feedbackDir, 'feedback-log.jsonl');
32
- if (!fs.existsSync(logPath)) return true;
33
- try {
34
- const stats = fs.statSync(logPath);
35
- const minutesSinceModified = (Date.now() - stats.mtimeMs) / (1000 * 60);
36
- return minutesSinceModified >= thresholdMinutes;
37
- } catch { return true; }
38
- }
39
-
40
- function runSlowLoop({ thresholdMinutes = IDLE_THRESHOLD_MINUTES, force = false } = {}) {
41
- const state = loadState();
42
- const idle = force || isIdle({ thresholdMinutes });
43
- if (!idle) { state.lastIdleCheckAt = new Date().toISOString(); saveState(state); return { action: 'skipped', reason: 'system not idle', idle: false, state }; }
44
-
45
- const feedbackDir = resolveFeedbackDir();
46
- const logPath = path.join(feedbackDir, 'feedback-log.jsonl');
47
- let newEntries = 0;
48
- if (fs.existsSync(logPath)) {
49
- const totalEntries = fs.readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean).length;
50
- newEntries = totalEntries - (state.lastFeedbackCount || 0);
51
- state.lastFeedbackCount = totalEntries;
52
- }
53
- if (newEntries <= 0 && !force) { state.lastIdleCheckAt = new Date().toISOString(); saveState(state); return { action: 'skipped', reason: 'no new feedback since last export', idle: true, newEntries: 0, state }; }
54
-
55
- let dpoResult = null;
56
- try { const { exportDpoPairs } = require('./feedback-loop'); dpoResult = exportDpoPairs(); } catch (err) { dpoResult = { error: err.message, pairsExported: 0 }; }
57
-
58
- const pairsExported = dpoResult && dpoResult.pairs ? dpoResult.pairs.length : (dpoResult && dpoResult.pairsExported) || 0;
59
- state.lastExportAt = new Date().toISOString();
60
- state.exportCount = (state.exportCount || 0) + 1;
61
- state.totalPairsExported = (state.totalPairsExported || 0) + pairsExported;
62
- state.lastIdleCheckAt = new Date().toISOString();
63
- saveState(state);
64
- return { action: 'exported', idle: true, newEntries, pairsExported, totalExports: state.exportCount, totalPairsExported: state.totalPairsExported, exportedAt: state.lastExportAt, state };
65
- }
66
-
67
- function createSlowLoopSchedule({ schedule = 'hourly', thresholdMinutes = IDLE_THRESHOLD_MINUTES } = {}) {
68
- const command = [`const sl = require(${JSON.stringify(__filename)});`, `const result = sl.runSlowLoop(${JSON.stringify({ thresholdMinutes })});`, 'process.stdout.write(JSON.stringify(result, null, 2) + "\\n");'].join(' ');
69
- return createSchedule({ id: 'thumbgate-slow-loop', name: 'ThumbGate Slow Loop (DPO Export)', description: `Idle-time DPO export, runs ${schedule}`, schedule, command });
70
- }
71
-
72
- module.exports = { isIdle, runSlowLoop, createSlowLoopSchedule, loadState, getStatePath };
File without changes
@@ -1,32 +0,0 @@
1
- CREATE TABLE IF NOT EXISTS engagement_metrics (
2
- id INTEGER PRIMARY KEY AUTOINCREMENT,
3
- platform TEXT NOT NULL,
4
- content_type TEXT NOT NULL,
5
- post_id TEXT NOT NULL,
6
- post_url TEXT,
7
- published_at TEXT,
8
- metric_date TEXT NOT NULL,
9
- impressions INTEGER DEFAULT 0,
10
- reach INTEGER DEFAULT 0,
11
- likes INTEGER DEFAULT 0,
12
- comments INTEGER DEFAULT 0,
13
- shares INTEGER DEFAULT 0,
14
- saves INTEGER DEFAULT 0,
15
- clicks INTEGER DEFAULT 0,
16
- video_views INTEGER DEFAULT 0,
17
- followers_delta INTEGER DEFAULT 0,
18
- extra_json TEXT,
19
- fetched_at TEXT NOT NULL,
20
- UNIQUE(platform, post_id, metric_date)
21
- );
22
-
23
- CREATE TABLE IF NOT EXISTS follower_snapshots (
24
- id INTEGER PRIMARY KEY AUTOINCREMENT,
25
- platform TEXT NOT NULL,
26
- follower_count INTEGER NOT NULL,
27
- snapshot_date TEXT NOT NULL,
28
- UNIQUE(platform, snapshot_date)
29
- );
30
-
31
- CREATE INDEX IF NOT EXISTS idx_metrics_platform_date ON engagement_metrics(platform, metric_date);
32
- CREATE INDEX IF NOT EXISTS idx_metrics_published ON engagement_metrics(published_at);