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,405 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Filesystem Search — Embedding-Free Knowledge Retrieval
5
+ *
6
+ * Replaces LanceDB vector similarity search with deterministic filesystem
7
+ * operations (grep, token matching, recency weighting) over existing JSONL
8
+ * and ContextFS data. Inspired by Vercel's "knowledge agents without embeddings"
9
+ * approach: give the agent a filesystem and bash instead of vectors.
10
+ *
11
+ * Advantages over vector-store.js:
12
+ * - Zero binary dependencies (no LanceDB, no HuggingFace ONNX)
13
+ * - Deterministic, inspectable, debuggable retrieval
14
+ * - Works immediately without embedding model download
15
+ * - ~75% cheaper (no embedding compute)
16
+ * - Data is always the JSONL source of truth (no sync drift)
17
+ */
18
+
19
+ const fs = require('node:fs');
20
+ const path = require('node:path');
21
+ const crypto = require('node:crypto');
22
+ const { resolveFeedbackDir } = require('./feedback-paths');
23
+
24
+ const PROJECT_ROOT = path.join(__dirname, '..');
25
+ const DEFAULT_FEEDBACK_DIR = resolveFeedbackDir();
26
+ const DEFAULT_CONTEXTFS_DIR = path.join(DEFAULT_FEEDBACK_DIR, 'contextfs');
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Core utilities
30
+ // ---------------------------------------------------------------------------
31
+
32
+ function getFeedbackDir() {
33
+ return resolveFeedbackDir();
34
+ }
35
+
36
+ function getContextFsDir() {
37
+ return process.env.THUMBGATE_CONTEXTFS_DIR || path.join(getFeedbackDir(), 'contextfs');
38
+ }
39
+
40
+ function readJsonl(filePath) {
41
+ if (!fs.existsSync(filePath)) return [];
42
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
43
+ if (!raw) return [];
44
+ return raw.split('\n').map((line) => {
45
+ try { return JSON.parse(line); } catch { return null; }
46
+ }).filter(Boolean);
47
+ }
48
+
49
+ function listJsonFiles(dirPath) {
50
+ if (!fs.existsSync(dirPath)) return [];
51
+ const results = [];
52
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ const full = path.join(dirPath, entry.name);
55
+ if (entry.isDirectory()) {
56
+ results.push(...listJsonFiles(full));
57
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
58
+ results.push(full);
59
+ }
60
+ }
61
+ return results;
62
+ }
63
+
64
+ function tokenize(text) {
65
+ return String(text || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
66
+ }
67
+
68
+ function unique(arr) {
69
+ return [...new Set(arr)];
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Scoring: token overlap + recency boost + signal weighting
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function jaccardSimilarity(tokensA, tokensB) {
77
+ const setA = new Set(unique(tokensA));
78
+ const setB = new Set(unique(tokensB));
79
+ if (setA.size === 0 && setB.size === 0) return 1;
80
+ if (setA.size === 0 || setB.size === 0) return 0;
81
+ let intersection = 0;
82
+ for (const t of setA) {
83
+ if (setB.has(t)) intersection++;
84
+ }
85
+ const union = setA.size + setB.size - intersection;
86
+ return union === 0 ? 0 : intersection / union;
87
+ }
88
+
89
+ function substringBoost(query, text) {
90
+ const q = query.toLowerCase();
91
+ const t = text.toLowerCase();
92
+ if (t.includes(q)) return 0.3;
93
+ const words = q.split(/\s+/).filter((w) => w.length > 2);
94
+ const matched = words.filter((w) => t.includes(w)).length;
95
+ return words.length > 0 ? (matched / words.length) * 0.2 : 0;
96
+ }
97
+
98
+ function recencyScore(timestamp) {
99
+ if (!timestamp) return 0;
100
+ const ms = typeof timestamp === 'number' ? timestamp : new Date(timestamp).getTime();
101
+ if (!Number.isFinite(ms)) return 0;
102
+ const ageHours = (Date.now() - ms) / (1000 * 60 * 60);
103
+ if (ageHours <= 24) return 0.15;
104
+ if (ageHours <= 168) return 0.1;
105
+ if (ageHours <= 720) return 0.05;
106
+ return 0;
107
+ }
108
+
109
+ function scoreRecord(queryTokens, queryText, record) {
110
+ const recordText = [
111
+ record.context || '',
112
+ record.whatWentWrong || record.what_went_wrong || '',
113
+ record.whatWorked || record.what_worked || '',
114
+ record.whatToChange || record.what_to_change || '',
115
+ (record.tags || []).join(' '),
116
+ record.pattern || '',
117
+ record.message || '',
118
+ record.query || '',
119
+ record.outcome || '',
120
+ ].filter(Boolean).join(' ');
121
+
122
+ const recordTokens = tokenize(recordText);
123
+ const jaccard = jaccardSimilarity(queryTokens, recordTokens);
124
+ const substr = substringBoost(queryText, recordText);
125
+ const recency = recencyScore(record.timestamp);
126
+ const signalBoost = record.signal === 'down' ? 0.05 : 0;
127
+
128
+ return {
129
+ score: jaccard + substr + recency + signalBoost,
130
+ record,
131
+ matchedTokens: unique(queryTokens).filter((t) => new Set(recordTokens).has(t)),
132
+ };
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Search functions (drop-in replacements for vector-store.js exports)
137
+ // ---------------------------------------------------------------------------
138
+
139
+ function searchFeedbackLog(queryText, limit = 5, options = {}) {
140
+ const logPath = path.join(getFeedbackDir(), 'feedback-log.jsonl');
141
+ let records = readJsonl(logPath);
142
+
143
+ // SQLite fallback: if JSONL is empty/tiny, pull records from the lesson DB
144
+ if (records.length <= 1) {
145
+ try {
146
+ const { initDB } = require('./lesson-db');
147
+ const db = initDB();
148
+ const rows = db.prepare('SELECT * FROM lessons ORDER BY timestamp DESC LIMIT 500').all();
149
+ if (rows.length > records.length) {
150
+ records = rows.map((r) => ({
151
+ id: r.id,
152
+ signal: r.signal,
153
+ context: r.context,
154
+ title: r.title || r.context,
155
+ tags: r.tags ? JSON.parse(r.tags) : [],
156
+ timestamp: r.timestamp,
157
+ whatWentWrong: r.what_went_wrong,
158
+ whatWorked: r.what_worked,
159
+ whatToChange: r.what_to_change,
160
+ }));
161
+ }
162
+ } catch { /* lesson-db not available */ }
163
+ }
164
+
165
+ // Wildcard query: return all records sorted by recency
166
+ const isWildcard = queryText === '*' || queryText === '';
167
+ const queryTokens = isWildcard ? [] : tokenize(queryText);
168
+
169
+ let scored = isWildcard
170
+ ? records.map((r) => ({ score: recencyScore(r.timestamp) || 0.01, record: r, matchedTokens: [] }))
171
+ : records.map((r) => scoreRecord(queryTokens, queryText, r));
172
+
173
+ if (options.where) {
174
+ scored = scored.filter((s) => {
175
+ if (options.where.signal && s.record.signal !== options.where.signal) return false;
176
+ if (options.where.tags) {
177
+ const requiredTags = options.where.tags.split(',').map((t) => t.trim());
178
+ const recordTags = s.record.tags || [];
179
+ if (!requiredTags.some((rt) => recordTags.includes(rt))) return false;
180
+ }
181
+ return true;
182
+ });
183
+ }
184
+
185
+ return scored
186
+ .filter((s) => s.score > 0)
187
+ .sort((a, b) => b.score - a.score)
188
+ .slice(0, limit)
189
+ .map((s) => ({
190
+ ...s.record,
191
+ _score: s.score,
192
+ _matchedTokens: s.matchedTokens,
193
+ }));
194
+ }
195
+
196
+ function searchContextFs(queryText, limit = 5, options = {}) {
197
+ const contextDir = getContextFsDir();
198
+ const namespaces = options.namespaces || ['memory/error', 'memory/learning', 'rules', 'raw_history'];
199
+ const queryTokens = tokenize(queryText);
200
+ const scored = [];
201
+
202
+ for (const ns of namespaces) {
203
+ const nsDir = path.join(contextDir, ns);
204
+ const files = listJsonFiles(nsDir);
205
+
206
+ for (const filePath of files) {
207
+ try {
208
+ const raw = fs.readFileSync(filePath, 'utf-8');
209
+ const record = JSON.parse(raw);
210
+ record._source = path.relative(contextDir, filePath);
211
+ record._namespace = ns;
212
+ const result = scoreRecord(queryTokens, queryText, record);
213
+ if (result.score > 0) {
214
+ scored.push(result);
215
+ }
216
+ } catch {
217
+ // Skip malformed files
218
+ }
219
+ }
220
+ }
221
+
222
+ return scored
223
+ .sort((a, b) => b.score - a.score)
224
+ .slice(0, limit)
225
+ .map((s) => ({
226
+ ...s.record,
227
+ _score: s.score,
228
+ _matchedTokens: s.matchedTokens,
229
+ }));
230
+ }
231
+
232
+ function searchPreventionRules(queryText, limit = 5) {
233
+ const rulesPath = path.join(getFeedbackDir(), 'prevention-rules.md');
234
+ if (!fs.existsSync(rulesPath)) return [];
235
+
236
+ const content = fs.readFileSync(rulesPath, 'utf-8');
237
+ const queryTokens = tokenize(queryText);
238
+ const blocks = content.split(/^#{1,3}\s+/m).filter(Boolean);
239
+
240
+ return blocks
241
+ .map((block) => {
242
+ const lines = block.trim().split('\n');
243
+ const title = lines[0] || '';
244
+ const body = lines.slice(1).join('\n').trim();
245
+ const tokens = tokenize(`${title} ${body}`);
246
+ const jaccard = jaccardSimilarity(queryTokens, tokens);
247
+ const substr = substringBoost(queryText, `${title} ${body}`);
248
+ return { title, body, score: jaccard + substr };
249
+ })
250
+ .filter((r) => r.score > 0)
251
+ .sort((a, b) => b.score - a.score)
252
+ .slice(0, limit)
253
+ .map((r) => ({ ...r, _score: r.score }));
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Unified search — searches all sources and merges results
258
+ // ---------------------------------------------------------------------------
259
+
260
+ function searchAll(queryText, limit = 10, options = {}) {
261
+ const feedbackResults = searchFeedbackLog(queryText, limit, options);
262
+ const contextResults = searchContextFs(queryText, limit, options);
263
+ const ruleResults = searchPreventionRules(queryText, limit);
264
+
265
+ const merged = [
266
+ ...feedbackResults.map((r) => ({ ...r, _source_type: 'feedback' })),
267
+ ...contextResults.map((r) => ({ ...r, _source_type: 'contextfs' })),
268
+ ...ruleResults.map((r) => ({ ...r, _source_type: 'prevention_rule' })),
269
+ ];
270
+
271
+ return merged
272
+ .sort((a, b) => (b._score || 0) - (a._score || 0))
273
+ .slice(0, limit);
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Drop-in compatibility with vector-store.js interface
278
+ // ---------------------------------------------------------------------------
279
+
280
+ async function searchSimilar(queryText, limit = 5, options = {}) {
281
+ return searchAll(queryText, limit, options);
282
+ }
283
+
284
+ async function upsertFeedback(feedbackEvent) {
285
+ // No-op: feedback is already written to JSONL by feedback-loop.js.
286
+ // The filesystem IS the index. No separate upsert needed.
287
+ // Yield to microtask queue so trackBackgroundSideEffect captures a pending promise.
288
+ await Promise.resolve();
289
+ return feedbackEvent;
290
+ }
291
+
292
+ async function upsertPreventionRule(_rule) {
293
+ // No-op: prevention rules are already in prevention-rules.md
294
+ return _rule;
295
+ }
296
+
297
+ async function upsertContextPack(_pack) {
298
+ // No-op: context packs are already in contextfs/
299
+ return _pack;
300
+ }
301
+
302
+ async function searchPreventionRulesCompat(queryText, limit = 5, _options = {}) {
303
+ return searchPreventionRules(queryText, limit);
304
+ }
305
+
306
+ async function searchContextPacks(queryText, limit = 5, options = {}) {
307
+ return searchContextFs(queryText, limit, {
308
+ ...options,
309
+ namespaces: options.namespaces || ['session'],
310
+ });
311
+ }
312
+
313
+ function getEmbeddingConfig() {
314
+ return {
315
+ selectedProfile: { id: 'filesystem', model: 'none', quantized: false, maxChars: Infinity },
316
+ fallbackProfile: { id: 'filesystem', model: 'none', quantized: false, maxChars: Infinity },
317
+ reason: 'Filesystem search — no embeddings needed',
318
+ };
319
+ }
320
+
321
+ function getLastEmbeddingProfile() {
322
+ return {
323
+ activeProfile: { id: 'filesystem', model: 'none', quantized: false },
324
+ fallbackUsed: false,
325
+ reason: 'Filesystem search — no embeddings needed',
326
+ };
327
+ }
328
+
329
+ function getVersionSnapshot() {
330
+ return Promise.resolve({
331
+ rlhf_memories: null,
332
+ prevention_rules: null,
333
+ context_packs: null,
334
+ engine: 'filesystem-search',
335
+ });
336
+ }
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // Stats / diagnostics
340
+ // ---------------------------------------------------------------------------
341
+
342
+ function getSearchStats() {
343
+ const feedbackPath = path.join(getFeedbackDir(), 'feedback-log.jsonl');
344
+ const contextDir = getContextFsDir();
345
+ const rulesPath = path.join(getFeedbackDir(), 'prevention-rules.md');
346
+
347
+ return {
348
+ engine: 'filesystem-search',
349
+ feedbackEntries: readJsonl(feedbackPath).length,
350
+ contextFsFiles: listJsonFiles(contextDir).length,
351
+ preventionRulesExist: fs.existsSync(rulesPath),
352
+ feedbackDir: getFeedbackDir(),
353
+ contextFsDir: contextDir,
354
+ };
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // CLI
359
+ // ---------------------------------------------------------------------------
360
+
361
+ if (require.main === module) {
362
+ const query = process.argv.slice(2).join(' ') || 'test failure';
363
+ console.log(`Searching for: "${query}"`);
364
+ console.log('');
365
+
366
+ const stats = getSearchStats();
367
+ console.log('Search stats:', JSON.stringify(stats, null, 2));
368
+ console.log('');
369
+
370
+ const results = searchAll(query, 5);
371
+ console.log(`Found ${results.length} results:`);
372
+ results.forEach((r, i) => {
373
+ console.log(` ${i + 1}. [${r._source_type}] score=${r._score?.toFixed(3)} tokens=${(r._matchedTokens || []).join(',')}`);
374
+ if (r.context) console.log(` context: ${r.context.slice(0, 120)}`);
375
+ if (r.title) console.log(` rule: ${r.title}`);
376
+ });
377
+ }
378
+
379
+ module.exports = {
380
+ // Primary search API
381
+ searchFeedbackLog,
382
+ searchContextFs,
383
+ searchPreventionRulesSync: searchPreventionRules,
384
+ searchAll,
385
+ getSearchStats,
386
+
387
+ // Drop-in vector-store.js compatibility
388
+ searchSimilar,
389
+ upsertFeedback,
390
+ upsertPreventionRule,
391
+ searchPreventionRules: searchPreventionRulesCompat,
392
+ upsertContextPack,
393
+ searchContextPacks,
394
+ getEmbeddingConfig,
395
+ getLastEmbeddingProfile,
396
+ getVersionSnapshot,
397
+ TABLE_NAME: 'rlhf_memories',
398
+ TABLE_PREVENTION_RULES: 'prevention_rules',
399
+ TABLE_CONTEXT_PACKS: 'context_packs',
400
+
401
+ // Test helpers (no-ops — no pipeline to mock)
402
+ setPipelineLoaderForTests: () => {},
403
+ setLanceLoaderForTests: () => {},
404
+ truncateForEmbedding: (text) => String(text || ''),
405
+ };
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ const { getBusinessAnalytics } = require('./billing');
3
+
4
+ function generateFunnelReport() {
5
+ const analytics = getBusinessAnalytics();
6
+ console.log('╔══════════════════════════════════════════════════════╗');
7
+ console.log('║ Marketing & Revenue Funnel Analytics ║');
8
+ console.log('╠══════════════════════════════════════════════════════╣');
9
+ console.log(`║ Traffic Visitors: ${String((analytics.trafficMetrics && analytics.trafficMetrics.visitors) || 0).padStart(6)} ║`);
10
+ console.log(`║ CTA Clicks: ${String((analytics.trafficMetrics && analytics.trafficMetrics.ctaClicks) || 0).padStart(6)} ║`);
11
+ console.log(`║ Unique Leads: ${String(analytics.signups.uniqueLeads).padStart(6)} ║`);
12
+ console.log(`║ Sprint Leads: ${String((analytics.pipeline.workflowSprintLeads && analytics.pipeline.workflowSprintLeads.total) || 0).padStart(6)} ║`);
13
+ console.log(`║ Paid Provider Events: ${String(analytics.revenue.paidProviderEvents || 0).padStart(6)} ║`);
14
+ console.log(`║ Paid Orders Tracked: ${String(analytics.revenue.paidOrders).padStart(6)} ║`);
15
+ console.log(`║ Known Booked Revenue (cents): ${String(analytics.revenue.bookedRevenueCents).padStart(6)} ║`);
16
+ console.log('╠══════════════════════════════════════════════════════╣');
17
+ console.log('║ Acquisition by Source: ║');
18
+ Object.entries(analytics.signups.bySource).forEach(([key, count]) => {
19
+ const line = ` ${key}: ${count}`;
20
+ console.log(`║ ${line.padEnd(52)}║`);
21
+ });
22
+ console.log('║ Paid Orders by Source: ║');
23
+ Object.entries(analytics.attribution.paidBySource).forEach(([key, count]) => {
24
+ const line = ` ${key}: ${count}`;
25
+ console.log(`║ ${line.padEnd(52)}║`);
26
+ });
27
+ console.log('║ Revenue by Source (cents): ║');
28
+ Object.entries(analytics.attribution.bookedRevenueBySourceCents).forEach(([key, count]) => {
29
+ const line = ` ${key}: ${count}`;
30
+ console.log(`║ ${line.padEnd(52)}║`);
31
+ });
32
+ console.log('╚══════════════════════════════════════════════════════╝');
33
+ }
34
+ if (require.main === module) generateFunnelReport();
35
+ module.exports = { generateFunnelReport };
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { satisfyCondition } = require('./gates-engine');
5
+
6
+ function run() {
7
+ const args = process.argv.slice(2);
8
+ let gateId = null;
9
+ let evidence = '';
10
+
11
+ for (let i = 0; i < args.length; i++) {
12
+ if (args[i] === '--gate' && args[i + 1]) {
13
+ gateId = args[i + 1];
14
+ i++;
15
+ } else if (args[i] === '--evidence' && args[i + 1]) {
16
+ evidence = args[i + 1];
17
+ i++;
18
+ }
19
+ }
20
+
21
+ if (!gateId) {
22
+ process.stderr.write('Usage: node scripts/gate-satisfy.js --gate <gate-id> [--evidence "<text>"]\n');
23
+ process.exit(1);
24
+ }
25
+
26
+ const entry = satisfyCondition(gateId, evidence);
27
+ const output = { gate: gateId, satisfied: true, timestamp: entry.timestamp, evidence: entry.evidence };
28
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
29
+ }
30
+
31
+ // Export for MCP tool usage
32
+ function satisfyGate(gateId, evidence) {
33
+ if (!gateId) throw new Error('gate ID is required');
34
+ const entry = satisfyCondition(gateId, evidence || '');
35
+ return { gate: gateId, satisfied: true, timestamp: entry.timestamp, evidence: entry.evidence };
36
+ }
37
+
38
+ module.exports = { satisfyGate };
39
+
40
+ if (require.main === module) {
41
+ run();
42
+ }
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { getAutoGatesPath } = require('./auto-promote-gates');
7
+
8
+ const PROJECT_ROOT = path.join(__dirname, '..');
9
+ const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
10
+
11
+ function loadGatesFile(filePath) {
12
+ if (!fs.existsSync(filePath)) return [];
13
+ try {
14
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
15
+ return data.gates || [];
16
+ } catch {
17
+ return [];
18
+ }
19
+ }
20
+
21
+ function calculateStats() {
22
+ const autoGatesPath = getAutoGatesPath();
23
+ const manualGates = loadGatesFile(MANUAL_GATES_PATH);
24
+ const autoGates = loadGatesFile(autoGatesPath);
25
+ const allGates = [...manualGates, ...autoGates];
26
+
27
+ let autoPromotedData = { promotionLog: [] };
28
+ if (fs.existsSync(autoGatesPath)) {
29
+ try { autoPromotedData = JSON.parse(fs.readFileSync(autoGatesPath, 'utf-8')); } catch {}
30
+ }
31
+ const promotionLog = autoPromotedData.promotionLog || [];
32
+
33
+ const blockGates = allGates.filter((g) => g.action === 'block');
34
+ const warnGates = allGates.filter((g) => g.action === 'warn');
35
+
36
+ // Count total blocks/warns from occurrences in auto-promoted gates
37
+ const totalBlocked = autoGates
38
+ .filter((g) => g.action === 'block')
39
+ .reduce((sum, g) => sum + (g.occurrences || 0), 0);
40
+ const totalWarned = autoGates
41
+ .filter((g) => g.action === 'warn')
42
+ .reduce((sum, g) => sum + (g.occurrences || 0), 0);
43
+
44
+ // Top blocked gate
45
+ const topBlocked = [...allGates]
46
+ .sort((a, b) => (b.occurrences || 0) - (a.occurrences || 0))
47
+ .find((g) => g.action === 'block') || null;
48
+
49
+ // Last promotion event
50
+ const lastPromotion = promotionLog.length > 0
51
+ ? promotionLog[promotionLog.length - 1]
52
+ : null;
53
+
54
+ // Time saved estimate: ~15 min per blocked mistake
55
+ const estimatedMinutesSaved = (totalBlocked + totalWarned) * 15;
56
+ const estimatedHoursSaved = (estimatedMinutesSaved / 60).toFixed(1);
57
+
58
+ return {
59
+ totalGates: allGates.length,
60
+ manualGates: manualGates.length,
61
+ autoPromotedGates: autoGates.length,
62
+ blockGates: blockGates.length,
63
+ warnGates: warnGates.length,
64
+ totalBlocked,
65
+ totalWarned,
66
+ topBlocked,
67
+ lastPromotion,
68
+ estimatedHoursSaved,
69
+ gates: allGates,
70
+ };
71
+ }
72
+
73
+ function formatLastPromotion(promo) {
74
+ if (!promo) return 'none';
75
+ const ts = promo.timestamp ? new Date(promo.timestamp) : null;
76
+ if (!ts) return `${promo.gateId || 'unknown'}`;
77
+ const now = Date.now();
78
+ const diffMs = now - ts.getTime();
79
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
80
+ const ago = diffDays === 0 ? 'today' : diffDays === 1 ? '1 day ago' : `${diffDays} days ago`;
81
+ const label = promo.type === 'upgrade'
82
+ ? `${promo.gateId} -> ${promo.to} (${ago})`
83
+ : `${promo.gateId} (${ago})`;
84
+ return label;
85
+ }
86
+
87
+ function formatStats(stats) {
88
+ const lines = [];
89
+ lines.push('Gate Statistics');
90
+ lines.push('─'.repeat(38));
91
+ lines.push(` Active gates: ${stats.totalGates} (${stats.manualGates} manual, ${stats.autoPromotedGates} auto-promoted)`);
92
+ lines.push(` Actions blocked: ${stats.totalBlocked}`);
93
+ lines.push(` Actions warned: ${stats.totalWarned}`);
94
+ lines.push(` Top blocked gate: ${stats.topBlocked ? `${stats.topBlocked.id} (${stats.topBlocked.occurrences || 0} blocks)` : 'none'}`);
95
+ lines.push(` Last promotion: ${formatLastPromotion(stats.lastPromotion)}`);
96
+ lines.push(` Estimated time saved: ~${stats.estimatedHoursSaved} hours`);
97
+ return lines.join('\n');
98
+ }
99
+
100
+ if (require.main === module) {
101
+ try {
102
+ const stats = calculateStats();
103
+ console.log('\n' + formatStats(stats) + '\n');
104
+ } catch (err) {
105
+ console.error('gate-stats error:', err.message);
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ module.exports = {
111
+ calculateStats,
112
+ formatStats,
113
+ formatLastPromotion,
114
+ loadGatesFile,
115
+ MANUAL_GATES_PATH,
116
+ };
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const ROOT = path.join(__dirname, '..');
8
+ const DEFAULT_TEMPLATES_PATH = path.join(ROOT, 'config', 'gate-templates.json');
9
+
10
+ function normalizeText(value) {
11
+ if (value === undefined || value === null) return null;
12
+ const text = String(value).trim();
13
+ return text || null;
14
+ }
15
+
16
+ function loadGateTemplates(filePath = DEFAULT_TEMPLATES_PATH) {
17
+ if (!fs.existsSync(filePath)) {
18
+ throw new Error(`Gate templates file not found: ${filePath}`);
19
+ }
20
+
21
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
22
+ if (!parsed || !Array.isArray(parsed.templates)) {
23
+ throw new Error(`Gate templates file ${filePath} is missing a "templates" array`);
24
+ }
25
+
26
+ return parsed;
27
+ }
28
+
29
+ function listGateTemplates(filePath = DEFAULT_TEMPLATES_PATH) {
30
+ const config = loadGateTemplates(filePath);
31
+ return config.templates.map((template) => ({
32
+ id: normalizeText(template.id),
33
+ name: normalizeText(template.name),
34
+ category: normalizeText(template.category),
35
+ signal: normalizeText(template.signal),
36
+ defaultAction: normalizeText(template.defaultAction),
37
+ severity: normalizeText(template.severity),
38
+ pattern: normalizeText(template.pattern),
39
+ problem: normalizeText(template.problem),
40
+ roi: normalizeText(template.roi),
41
+ rollout: normalizeText(template.rollout),
42
+ }));
43
+ }
44
+
45
+ function summarizeGateTemplates(filePath = DEFAULT_TEMPLATES_PATH) {
46
+ const templates = listGateTemplates(filePath);
47
+ const categories = {};
48
+ const byAction = {};
49
+
50
+ for (const template of templates) {
51
+ const category = template.category || 'Other';
52
+ categories[category] = (categories[category] || 0) + 1;
53
+ const action = template.defaultAction || 'unknown';
54
+ byAction[action] = (byAction[action] || 0) + 1;
55
+ }
56
+
57
+ return {
58
+ total: templates.length,
59
+ categories,
60
+ byAction,
61
+ templates,
62
+ };
63
+ }
64
+
65
+ module.exports = {
66
+ DEFAULT_TEMPLATES_PATH,
67
+ listGateTemplates,
68
+ loadGateTemplates,
69
+ summarizeGateTemplates,
70
+ };