thumbgate 0.9.10
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.
- package/.claude-plugin/README.md +134 -0
- package/.claude-plugin/bundle/icon.png +0 -0
- package/.claude-plugin/bundle/icon.svg +18 -0
- package/.claude-plugin/bundle/server/index.js +24 -0
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.well-known/mcp/server-card.json +231 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/adapters/README.md +9 -0
- package/adapters/amp/skills/thumbgate-feedback/SKILL.md +22 -0
- package/adapters/chatgpt/INSTALL.md +83 -0
- package/adapters/chatgpt/openapi.yaml +1281 -0
- package/adapters/claude/.mcp.json +14 -0
- package/adapters/codex/config.toml +9 -0
- package/adapters/gemini/function-declarations.json +224 -0
- package/adapters/mcp/server-stdio.js +788 -0
- package/adapters/opencode/opencode.json +15 -0
- package/bin/cli.js +1484 -0
- package/bin/memory.sh +64 -0
- package/bin/obsidian-sync.sh +20 -0
- package/bin/postinstall.js +37 -0
- package/config/build-metadata.json +4 -0
- package/config/e2e-critical-flows.json +45 -0
- package/config/gate-templates.json +77 -0
- package/config/gates/claim-verification.json +29 -0
- package/config/gates/computer-use.json +39 -0
- package/config/gates/default.json +117 -0
- package/config/github-about.json +25 -0
- package/config/mcp-allowlists.json +135 -0
- package/config/model-tiers.json +33 -0
- package/config/partner-routing.json +132 -0
- package/config/policy-bundles/constrained-v1.json +64 -0
- package/config/policy-bundles/default-v1.json +91 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/skill-packs/react-testing.json +23 -0
- package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
- package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
- package/config/skill-specs/pr-reviewer.json +9 -0
- package/config/skill-specs/release-status.json +9 -0
- package/config/skill-specs/ticket-triage.json +9 -0
- package/config/subagent-profiles.json +32 -0
- package/config/tessl-tiles.json +29 -0
- package/config/thumbgate-settings.managed.json +12 -0
- package/openapi/openapi.yaml +1281 -0
- package/package.json +283 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +64 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
- package/plugins/claude-codex-bridge/.mcp.json +12 -0
- package/plugins/claude-codex-bridge/INSTALL.md +43 -0
- package/plugins/claude-codex-bridge/README.md +46 -0
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
- package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
- package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
- package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
- package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
- package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
- package/plugins/codex-profile/.mcp.json +12 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +66 -0
- package/plugins/codex-profile/README.md +37 -0
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
- package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
- package/plugins/cursor-marketplace/LICENSE +21 -0
- package/plugins/cursor-marketplace/README.md +124 -0
- package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
- package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
- package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
- package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
- package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
- package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
- package/plugins/cursor-marketplace/mcp.json +12 -0
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
- package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
- package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
- package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
- package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
- package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
- package/plugins/gemini-extension/INSTALL.md +92 -0
- package/plugins/gemini-extension/gemini_prompt.txt +14 -0
- package/plugins/gemini-extension/tool_contract.json +45 -0
- package/plugins/opencode-profile/INSTALL.md +57 -0
- package/public/assets/instagram-card.png +0 -0
- package/public/assets/tiktok-agent-memory.mp4 +0 -0
- package/public/blog.html +400 -0
- package/public/dashboard.html +1093 -0
- package/public/guide.html +317 -0
- package/public/index.html +1014 -0
- package/public/learn/agent-harness-pattern.html +180 -0
- package/public/learn/ai-agent-persistent-memory.html +202 -0
- package/public/learn/learn.css +45 -0
- package/public/learn/mcp-pre-action-gates-explained.html +172 -0
- package/public/learn/stop-ai-agent-force-push.html +134 -0
- package/public/learn/vibe-coding-safety-net.html +142 -0
- package/public/learn.html +213 -0
- package/public/lessons.html +650 -0
- package/public/vercel.json +8 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/access-anomaly-detector.js +12 -0
- package/scripts/adk-consolidator.js +266 -0
- package/scripts/agent-readiness.js +220 -0
- package/scripts/agent-security-hardening.js +227 -0
- package/scripts/agentic-data-pipeline.js +847 -0
- package/scripts/analytics-report.js +328 -0
- package/scripts/analytics-window.js +158 -0
- package/scripts/async-job-runner.js +1001 -0
- package/scripts/audit-trail.js +398 -0
- package/scripts/auto-promote-gates.js +299 -0
- package/scripts/auto-wire-hooks.js +312 -0
- package/scripts/autonomous-sales-agent.js +39 -0
- package/scripts/autoresearch-runner.js +216 -0
- package/scripts/background-agent-governance.js +237 -0
- package/scripts/behavioral-extraction.js +97 -0
- package/scripts/belief-update.js +84 -0
- package/scripts/billing.js +2438 -0
- package/scripts/bot-detector.js +50 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/build-claude-mcpb.js +189 -0
- package/scripts/build-metadata.js +97 -0
- package/scripts/check-congruence.js +322 -0
- package/scripts/cli-feedback.js +135 -0
- package/scripts/cli-telemetry.js +87 -0
- package/scripts/cloudflare-dynamic-sandbox.js +315 -0
- package/scripts/code-reasoning.js +350 -0
- package/scripts/codegraph-context.js +466 -0
- package/scripts/commercial-offer.js +56 -0
- package/scripts/computer-use-firewall.js +250 -0
- package/scripts/context-engine.js +694 -0
- package/scripts/contextfs.js +1287 -0
- package/scripts/conversation-context.js +119 -0
- package/scripts/creator-campaigns.js +239 -0
- package/scripts/daemon-manager.js +108 -0
- package/scripts/daily-digest.js +11 -0
- package/scripts/dashboard-render-spec.js +395 -0
- package/scripts/dashboard.js +1058 -0
- package/scripts/data-governance.js +173 -0
- package/scripts/delegation-runtime.js +900 -0
- package/scripts/deploy-gcp.sh +44 -0
- package/scripts/deploy-policy.js +263 -0
- package/scripts/disagreement-mining.js +315 -0
- package/scripts/dispatch-brief.js +159 -0
- package/scripts/distribution-surfaces.js +44 -0
- package/scripts/dpo-optimizer.js +209 -0
- package/scripts/ephemeral-agent-store.js +219 -0
- package/scripts/eval-harness.js +56 -0
- package/scripts/evolution-state.js +241 -0
- package/scripts/experiment-tracker.js +267 -0
- package/scripts/export-databricks-bundle.js +242 -0
- package/scripts/export-dpo-pairs.js +345 -0
- package/scripts/export-kto-pairs.js +310 -0
- package/scripts/export-training.js +448 -0
- package/scripts/failure-diagnostics.js +558 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-fallback.js +111 -0
- package/scripts/feedback-history-distiller.js +391 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +1887 -0
- package/scripts/feedback-paths.js +145 -0
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-root-consolidator.js +238 -0
- package/scripts/feedback-schema.js +426 -0
- package/scripts/feedback-session.js +286 -0
- package/scripts/feedback-to-memory.js +185 -0
- package/scripts/feedback-to-rules.js +163 -0
- package/scripts/filesystem-search.js +404 -0
- package/scripts/funnel-analytics.js +35 -0
- package/scripts/gate-satisfy.js +42 -0
- package/scripts/gate-stats.js +116 -0
- package/scripts/gate-templates.js +70 -0
- package/scripts/gates-engine.js +816 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/generate-pretool-hook.sh +40 -0
- package/scripts/github-about.js +350 -0
- package/scripts/github-outreach.js +65 -0
- package/scripts/gtm-revenue-loop.js +520 -0
- package/scripts/hallucination-detector.js +226 -0
- package/scripts/hf-papers.js +317 -0
- package/scripts/history-distiller.js +200 -0
- package/scripts/hook-auto-capture.sh +95 -0
- package/scripts/hook-stop-pr-thread-check.sh +68 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/hook-stop-verify-deploy.sh +31 -0
- package/scripts/hook-thumbgate-cache-updater.js +48 -0
- package/scripts/hook-verify-before-done.sh +20 -0
- package/scripts/hosted-config.js +170 -0
- package/scripts/hybrid-feedback-context.js +676 -0
- package/scripts/install-mcp.js +159 -0
- package/scripts/intent-router.js +392 -0
- package/scripts/internal-agent-bootstrap.js +490 -0
- package/scripts/jsonl-watcher.js +155 -0
- package/scripts/lesson-db.js +613 -0
- package/scripts/lesson-inference.js +315 -0
- package/scripts/lesson-retrieval.js +95 -0
- package/scripts/lesson-rotation.js +137 -0
- package/scripts/lesson-search.js +644 -0
- package/scripts/lesson-synthesis.js +196 -0
- package/scripts/license.js +50 -0
- package/scripts/local-model-profile.js +383 -0
- package/scripts/markdown-escape.js +12 -0
- package/scripts/marketing-experiment.js +671 -0
- package/scripts/mcp-config.js +149 -0
- package/scripts/mcp-policy.js +99 -0
- package/scripts/memalign-recall.js +111 -0
- package/scripts/memory-firewall.js +222 -0
- package/scripts/memory-migration.js +296 -0
- package/scripts/meta-policy.js +194 -0
- package/scripts/metered-billing.js +16 -0
- package/scripts/model-tier-router.js +301 -0
- package/scripts/money-watcher.js +71 -0
- package/scripts/multi-hop-recall.js +240 -0
- package/scripts/natural-language-harness.js +330 -0
- package/scripts/obsidian-export.js +712 -0
- package/scripts/operational-dashboard.js +103 -0
- package/scripts/operational-summary.js +93 -0
- package/scripts/optimize-context.js +17 -0
- package/scripts/org-dashboard.js +201 -0
- package/scripts/partner-orchestration.js +146 -0
- package/scripts/per-step-scoring.js +165 -0
- package/scripts/perplexity-marketing.js +466 -0
- package/scripts/pii-scanner.js +153 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/post-everywhere.js +308 -0
- package/scripts/post-to-x-retry.sh +22 -0
- package/scripts/post-to-x.js +369 -0
- package/scripts/pr-manager.js +236 -0
- package/scripts/predictive-insights.js +356 -0
- package/scripts/principle-extractor.js +162 -0
- package/scripts/pro-features.js +40 -0
- package/scripts/pro-local-dashboard.js +174 -0
- package/scripts/problem-detail.js +53 -0
- package/scripts/product-feedback.js +134 -0
- package/scripts/profile-router.js +245 -0
- package/scripts/prompt-dlp.js +221 -0
- package/scripts/prompt-guard.js +83 -0
- package/scripts/prove-adapters.js +863 -0
- package/scripts/prove-attribution.js +365 -0
- package/scripts/prove-automation.js +653 -0
- package/scripts/prove-autoresearch.js +304 -0
- package/scripts/prove-claim-verification.js +277 -0
- package/scripts/prove-cloudflare-sandbox.js +163 -0
- package/scripts/prove-data-pipeline.js +410 -0
- package/scripts/prove-data-quality.js +227 -0
- package/scripts/prove-evolution.js +352 -0
- package/scripts/prove-harnesses.js +287 -0
- package/scripts/prove-intelligence.js +259 -0
- package/scripts/prove-lancedb.js +371 -0
- package/scripts/prove-local-intelligence.js +342 -0
- package/scripts/prove-loop-closure.js +263 -0
- package/scripts/prove-predictive-insights.js +357 -0
- package/scripts/prove-runtime.js +350 -0
- package/scripts/prove-seo-gsd.js +234 -0
- package/scripts/prove-settings.js +279 -0
- package/scripts/prove-subway-upgrades.js +277 -0
- package/scripts/prove-tessl.js +229 -0
- package/scripts/prove-training-export.js +327 -0
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/prove-xmemory.js +332 -0
- package/scripts/publish-decision.js +133 -0
- package/scripts/pulse.js +80 -0
- package/scripts/rate-limiter.js +125 -0
- package/scripts/reddit-dm-outreach.js +182 -0
- package/scripts/reddit-monitor-cron.sh +26 -0
- package/scripts/reflector-agent.js +221 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/revenue-status.js +472 -0
- package/scripts/risk-scorer.js +458 -0
- package/scripts/rlaif-self-audit.js +129 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/schedule-manager.js +251 -0
- package/scripts/secret-scanner.js +414 -0
- package/scripts/self-heal.js +147 -0
- package/scripts/self-healing-check.js +188 -0
- package/scripts/semantic-layer.js +98 -0
- package/scripts/seo-gsd.js +1153 -0
- package/scripts/settings-hierarchy.js +214 -0
- package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
- package/scripts/skill-exporter.js +262 -0
- package/scripts/skill-generator.js +446 -0
- package/scripts/skill-materializer.js +134 -0
- package/scripts/skill-packs.js +136 -0
- package/scripts/skill-proposer.js +99 -0
- package/scripts/skill-quality-tracker.js +284 -0
- package/scripts/slo-alert-engine.js +14 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/social-analytics/db/schema.sql +32 -0
- package/scripts/social-analytics/digest.js +256 -0
- package/scripts/social-analytics/generate-instagram-card.js +97 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +73 -0
- package/scripts/social-analytics/mcp-server.js +289 -0
- package/scripts/social-analytics/normalizer.js +580 -0
- package/scripts/social-analytics/notify.js +162 -0
- package/scripts/social-analytics/poll-all.js +107 -0
- package/scripts/social-analytics/pollers/github.js +195 -0
- package/scripts/social-analytics/pollers/instagram.js +253 -0
- package/scripts/social-analytics/pollers/linkedin.js +330 -0
- package/scripts/social-analytics/pollers/plausible.js +247 -0
- package/scripts/social-analytics/pollers/reddit.js +306 -0
- package/scripts/social-analytics/pollers/threads.js +233 -0
- package/scripts/social-analytics/pollers/tiktok.js +203 -0
- package/scripts/social-analytics/pollers/x.js +227 -0
- package/scripts/social-analytics/pollers/youtube.js +304 -0
- package/scripts/social-analytics/pollers/zernio.js +180 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +85 -0
- package/scripts/social-analytics/publishers/devto.js +122 -0
- package/scripts/social-analytics/publishers/instagram.js +317 -0
- package/scripts/social-analytics/publishers/linkedin.js +294 -0
- package/scripts/social-analytics/publishers/reddit.js +390 -0
- package/scripts/social-analytics/publishers/threads.js +275 -0
- package/scripts/social-analytics/publishers/tiktok.js +217 -0
- package/scripts/social-analytics/publishers/x.js +259 -0
- package/scripts/social-analytics/publishers/youtube.js +223 -0
- package/scripts/social-analytics/publishers/zernio.js +209 -0
- package/scripts/social-analytics/run-digest.js +34 -0
- package/scripts/social-analytics/store.js +257 -0
- package/scripts/social-analytics/utm.js +143 -0
- package/scripts/social-pipeline.js +2628 -0
- package/scripts/social-quality-gate.js +18 -0
- package/scripts/social-reply-monitor.js +445 -0
- package/scripts/status-dashboard.js +155 -0
- package/scripts/statusline-lesson.js +16 -0
- package/scripts/statusline-tower.js +8 -0
- package/scripts/statusline.sh +116 -0
- package/scripts/stripe-live-status.js +115 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +70 -0
- package/scripts/sync-github-about.js +52 -0
- package/scripts/sync-version.js +451 -0
- package/scripts/synthetic-dpo.js +234 -0
- package/scripts/telemetry-analytics.js +821 -0
- package/scripts/tessl-export.js +371 -0
- package/scripts/test-coverage.js +120 -0
- package/scripts/thompson-sampling.js +417 -0
- package/scripts/thumbgate-search.js +189 -0
- package/scripts/tool-kpi-tracker.js +12 -0
- package/scripts/tool-registry.js +811 -0
- package/scripts/train_from_feedback.py +910 -0
- package/scripts/user-profile.js +78 -0
- package/scripts/validate-feedback.js +580 -0
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +198 -0
- package/scripts/verification-loop.js +291 -0
- package/scripts/verify-obsidian-setup.sh +269 -0
- package/scripts/verify-run.js +269 -0
- package/scripts/webhook-delivery.js +62 -0
- package/scripts/weekly-auto-post.js +124 -0
- package/scripts/workflow-runs.js +154 -0
- package/scripts/workflow-sprint-intake.js +475 -0
- package/scripts/workspace-evolver.js +374 -0
- package/scripts/x-autonomous-marketing.js +139 -0
- package/scripts/xmemory-lite.js +405 -0
- package/skills/agent-memory/SKILL.md +97 -0
- package/skills/solve-architecture-autonomy/SKILL.md +17 -0
- package/skills/solve-architecture-autonomy/tool.js +33 -0
- package/skills/thumbgate/SKILL.md +114 -0
- package/skills/thumbgate-feedback/SKILL.md +49 -0
- package/src/api/server.js +4208 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Thompson Sampling Beta-Bernoulli Module
|
|
4
|
+
*
|
|
5
|
+
* Implements per-category reliability estimates (ML-01) and exponential
|
|
6
|
+
* time-decay weighting with half-life of 7 days (ML-02).
|
|
7
|
+
*
|
|
8
|
+
* Source: Direct port of train_from_feedback.py (Subway_RN_Demo) lines 218-293.
|
|
9
|
+
* Algorithm: Beta-Bernoulli update with Marsaglia-Tsang gamma sampling for
|
|
10
|
+
* posterior draws. Zero external npm dependencies.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const ts = require('./thompson-sampling');
|
|
14
|
+
* const model = ts.loadModel(modelPath);
|
|
15
|
+
* ts.updateModel(model, { signal: 'positive', timestamp: '...', categories: ['testing'] });
|
|
16
|
+
* const rel = ts.getReliability(model);
|
|
17
|
+
* const post = ts.samplePosteriors(model);
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const { parseTimestamp } = require('./feedback-schema');
|
|
24
|
+
const { getEffectiveSetting } = require('./evolution-state');
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Constants
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Exponential decay half-life in days. 2^(-age/HALF_LIFE_DAYS) weights recent feedback higher. */
|
|
31
|
+
const HALF_LIFE_DAYS = 7.0;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Minimum weight floor so that very old feedback still contributes (minimally),
|
|
35
|
+
* and invalid timestamps do not silently zero out updates.
|
|
36
|
+
*/
|
|
37
|
+
const DECAY_FLOOR = 0.01;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Minimum number of real samples before a category's posterior mean is trusted
|
|
41
|
+
* over the uniform prior. Below this threshold, getCalibration() marks the
|
|
42
|
+
* category as "uncalibrated" — callers should treat reliability estimates as
|
|
43
|
+
* speculative and fall back to rule-based decisions.
|
|
44
|
+
*/
|
|
45
|
+
const MIN_SAMPLES_THRESHOLD = 5;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Default category taxonomy — mirrors Subway's 8-keyword categories plus
|
|
49
|
+
* 'uncategorized' as the catch-all. Used when initializing a new model.
|
|
50
|
+
*/
|
|
51
|
+
const DEFAULT_CATEGORIES = [
|
|
52
|
+
'code_edit',
|
|
53
|
+
'git',
|
|
54
|
+
'testing',
|
|
55
|
+
'pr_review',
|
|
56
|
+
'search',
|
|
57
|
+
'architecture',
|
|
58
|
+
'security',
|
|
59
|
+
'debugging',
|
|
60
|
+
'product_recommendation',
|
|
61
|
+
'brand_compliance',
|
|
62
|
+
'sizing',
|
|
63
|
+
'pricing',
|
|
64
|
+
'regulatory',
|
|
65
|
+
'uncategorized',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Time-Decay Weight
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compute exponential time-decay weight for a feedback timestamp.
|
|
74
|
+
*
|
|
75
|
+
* Formula: weight = max(2^(-ageDays / HALF_LIFE_DAYS), DECAY_FLOOR)
|
|
76
|
+
*
|
|
77
|
+
* At age=0 days: weight ≈ 1.0
|
|
78
|
+
* At age=7 days: weight ≈ 0.5
|
|
79
|
+
* At age=∞ days: weight → DECAY_FLOOR (0.01)
|
|
80
|
+
*
|
|
81
|
+
* Returns DECAY_FLOOR for invalid/null timestamps so callers never receive 0.
|
|
82
|
+
*
|
|
83
|
+
* @param {string|null|undefined} timestamp - ISO 8601 timestamp string
|
|
84
|
+
* @returns {number} Weight in [DECAY_FLOOR, 1.0]
|
|
85
|
+
*/
|
|
86
|
+
function timeDecayWeight(timestamp) {
|
|
87
|
+
const d = parseTimestamp(timestamp);
|
|
88
|
+
const decayFloor = getEffectiveSetting('decay_floor', DECAY_FLOOR);
|
|
89
|
+
const halfLifeDays = getEffectiveSetting('half_life_days', HALF_LIFE_DAYS);
|
|
90
|
+
if (!d) return decayFloor;
|
|
91
|
+
const ageDays = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24);
|
|
92
|
+
return Math.max(Math.pow(2, -ageDays / halfLifeDays), decayFloor);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Model Lifecycle
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create a fresh Beta-Bernoulli model with uniform priors (alpha=1, beta=1)
|
|
101
|
+
* for all DEFAULT_CATEGORIES. The uniform prior encodes "no information yet."
|
|
102
|
+
*
|
|
103
|
+
* @returns {Object} Initial model object
|
|
104
|
+
*/
|
|
105
|
+
function createInitialModel() {
|
|
106
|
+
const now = new Date().toISOString();
|
|
107
|
+
const categories = {};
|
|
108
|
+
DEFAULT_CATEGORIES.forEach((cat) => {
|
|
109
|
+
categories[cat] = { alpha: 1.0, beta: 1.0, samples: 0, last_updated: null };
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
version: 1,
|
|
113
|
+
created: now,
|
|
114
|
+
updated: now,
|
|
115
|
+
total_entries: 0,
|
|
116
|
+
categories,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Load an existing model from disk. Falls back to createInitialModel() only
|
|
122
|
+
* if the file does not exist or contains invalid JSON.
|
|
123
|
+
*
|
|
124
|
+
* IMPORTANT: Never call createInitialModel() directly when you intend to
|
|
125
|
+
* update an existing model — that would reset all accumulated posteriors.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} modelPath - Absolute or relative path to feedback_model.json
|
|
128
|
+
* @returns {Object} Parsed model or fresh initial model
|
|
129
|
+
*/
|
|
130
|
+
function loadModel(modelPath) {
|
|
131
|
+
if (fs.existsSync(modelPath)) {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.parse(fs.readFileSync(modelPath, 'utf-8'));
|
|
134
|
+
} catch (_err) {
|
|
135
|
+
// Corrupt JSON — fall through to createInitialModel()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return createInitialModel();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Persist a model object to disk as formatted JSON.
|
|
143
|
+
*
|
|
144
|
+
* Creates parent directories if needed. Updates `model.updated` timestamp
|
|
145
|
+
* before writing so the file reflects the time of save.
|
|
146
|
+
*
|
|
147
|
+
* @param {Object} model - Model object to persist
|
|
148
|
+
* @param {string} modelPath - Absolute or relative path to write
|
|
149
|
+
*/
|
|
150
|
+
function saveModel(model, modelPath) {
|
|
151
|
+
model.updated = new Date().toISOString();
|
|
152
|
+
const dir = require('path').dirname(modelPath);
|
|
153
|
+
if (!fs.existsSync(dir)) {
|
|
154
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
fs.writeFileSync(modelPath, `${JSON.stringify(model, null, 2)}\n`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Model Update
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Apply a single weighted Beta-Bernoulli update to the model.
|
|
165
|
+
*
|
|
166
|
+
* For positive signal: alpha += timeDecayWeight(timestamp) * weightMultiplier
|
|
167
|
+
* For negative signal: beta += timeDecayWeight(timestamp) * weightMultiplier
|
|
168
|
+
*
|
|
169
|
+
* Updates all provided categories. If a category is not in the model yet,
|
|
170
|
+
* it is added with default priors before applying the update.
|
|
171
|
+
*
|
|
172
|
+
* Mutates model in place AND returns the model for chaining.
|
|
173
|
+
*
|
|
174
|
+
* @param {Object} model - Model object (mutated in place)
|
|
175
|
+
* @param {Object} params
|
|
176
|
+
* @param {'positive'|'negative'} params.signal - Feedback direction
|
|
177
|
+
* @param {string} params.timestamp - ISO 8601 timestamp for decay calculation
|
|
178
|
+
* @param {string[]} [params.categories] - Categories to update; defaults to ['uncategorized']
|
|
179
|
+
* @param {number} [params.weightMultiplier] - Optional multiplier for stronger/slower updates
|
|
180
|
+
* @returns {Object} The mutated model
|
|
181
|
+
*/
|
|
182
|
+
function updateModel(model, { signal, timestamp, categories, weightMultiplier, failureType }) {
|
|
183
|
+
const multiplier = Number.isFinite(weightMultiplier) && weightMultiplier > 0 ? weightMultiplier : 1;
|
|
184
|
+
const weight = timeDecayWeight(timestamp) * multiplier;
|
|
185
|
+
const isPositive = signal === 'positive';
|
|
186
|
+
const cats = categories && categories.length ? categories : ['uncategorized'];
|
|
187
|
+
|
|
188
|
+
cats.forEach((cat) => {
|
|
189
|
+
if (!model.categories[cat]) {
|
|
190
|
+
model.categories[cat] = { alpha: 1.0, beta: 1.0, samples: 0, last_updated: null };
|
|
191
|
+
}
|
|
192
|
+
if (isPositive) {
|
|
193
|
+
model.categories[cat].alpha += weight;
|
|
194
|
+
} else {
|
|
195
|
+
model.categories[cat].beta += weight;
|
|
196
|
+
}
|
|
197
|
+
model.categories[cat].samples += 1;
|
|
198
|
+
model.categories[cat].last_updated = timestamp;
|
|
199
|
+
|
|
200
|
+
// Dual-signal: update decision/execution sub-arms when failureType is provided
|
|
201
|
+
// Inspired by Gen-Searcher's dual reward (text + image) for more precise learning
|
|
202
|
+
if (failureType === 'decision' || failureType === 'execution') {
|
|
203
|
+
const subCat = `${cat}:${failureType}`;
|
|
204
|
+
if (!model.categories[subCat]) {
|
|
205
|
+
model.categories[subCat] = { alpha: 1.0, beta: 1.0, samples: 0, last_updated: null };
|
|
206
|
+
}
|
|
207
|
+
if (isPositive) {
|
|
208
|
+
model.categories[subCat].alpha += weight;
|
|
209
|
+
} else {
|
|
210
|
+
model.categories[subCat].beta += weight;
|
|
211
|
+
}
|
|
212
|
+
model.categories[subCat].samples += 1;
|
|
213
|
+
model.categories[subCat].last_updated = timestamp;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
model.total_entries = (model.total_entries || 0) + 1;
|
|
218
|
+
model.updated = new Date().toISOString();
|
|
219
|
+
return model;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Reliability Estimation
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Compute per-category reliability as the Beta posterior mean:
|
|
228
|
+
* reliability = alpha / (alpha + beta)
|
|
229
|
+
*
|
|
230
|
+
* With uniform priors (alpha=1, beta=1), reliability starts at 0.5.
|
|
231
|
+
* More positive signal → approaches 1.0.
|
|
232
|
+
* More negative signal → approaches 0.0.
|
|
233
|
+
*
|
|
234
|
+
* @param {Object} model - Model object containing categories
|
|
235
|
+
* @returns {Object} Map of category → { alpha, beta, reliability, samples }
|
|
236
|
+
*/
|
|
237
|
+
function getReliability(model) {
|
|
238
|
+
const results = {};
|
|
239
|
+
for (const [cat, params] of Object.entries(model.categories || {})) {
|
|
240
|
+
const total = params.alpha + params.beta;
|
|
241
|
+
results[cat] = {
|
|
242
|
+
alpha: params.alpha,
|
|
243
|
+
beta: params.beta,
|
|
244
|
+
reliability: total > 0 ? params.alpha / total : 0.5,
|
|
245
|
+
samples: params.samples,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Calibration
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check whether a single category has enough samples to be trusted.
|
|
257
|
+
*
|
|
258
|
+
* @param {Object} model - Model object containing categories
|
|
259
|
+
* @param {string} category - Category name to check
|
|
260
|
+
* @returns {boolean} true if samples >= MIN_SAMPLES_THRESHOLD
|
|
261
|
+
*/
|
|
262
|
+
function isCalibrated(model, category) {
|
|
263
|
+
const cat = model.categories && model.categories[category];
|
|
264
|
+
if (!cat) return false;
|
|
265
|
+
return cat.samples >= MIN_SAMPLES_THRESHOLD;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Return per-category calibration report: reliability, sample count,
|
|
270
|
+
* calibrated flag, and a confidence tier (high / medium / low / none).
|
|
271
|
+
*
|
|
272
|
+
* Confidence tiers:
|
|
273
|
+
* none — 0 samples (pure prior)
|
|
274
|
+
* low — 1–4 samples (speculative)
|
|
275
|
+
* medium — 5–19 samples (usable)
|
|
276
|
+
* high — 20+ samples (trustworthy)
|
|
277
|
+
*
|
|
278
|
+
* @param {Object} model - Model object containing categories
|
|
279
|
+
* @returns {Object} Map of category → { reliability, samples, calibrated, confidence }
|
|
280
|
+
*/
|
|
281
|
+
function getCalibration(model) {
|
|
282
|
+
const results = {};
|
|
283
|
+
for (const [cat, params] of Object.entries(model.categories || {})) {
|
|
284
|
+
const total = params.alpha + params.beta;
|
|
285
|
+
const reliability = total > 0 ? params.alpha / total : 0.5;
|
|
286
|
+
const samples = params.samples || 0;
|
|
287
|
+
const calibrated = samples >= MIN_SAMPLES_THRESHOLD;
|
|
288
|
+
|
|
289
|
+
let confidence;
|
|
290
|
+
if (samples === 0) confidence = 'none';
|
|
291
|
+
else if (samples < MIN_SAMPLES_THRESHOLD) confidence = 'low';
|
|
292
|
+
else if (samples < 20) confidence = 'medium';
|
|
293
|
+
else confidence = 'high';
|
|
294
|
+
|
|
295
|
+
results[cat] = { reliability, samples, calibrated, confidence };
|
|
296
|
+
}
|
|
297
|
+
return results;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Posterior Sampling
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Draw one sample from the Beta posterior for each category via the
|
|
306
|
+
* Marsaglia-Tsang (2000) gamma ratio method. No external library needed.
|
|
307
|
+
*
|
|
308
|
+
* betaSample(alpha, beta) = gammaSample(alpha) / (gammaSample(alpha) + gammaSample(beta))
|
|
309
|
+
*
|
|
310
|
+
* This is the JS equivalent of Python's random.betavariate(alpha, beta).
|
|
311
|
+
* Used for Thompson Sampling action selection (explore via uncertainty).
|
|
312
|
+
*
|
|
313
|
+
* @param {Object} model - Model object containing categories
|
|
314
|
+
* @returns {Object} Map of category → float sample in [0, 1]
|
|
315
|
+
*/
|
|
316
|
+
function samplePosteriors(model) {
|
|
317
|
+
const samples = {};
|
|
318
|
+
for (const [cat, params] of Object.entries(model.categories || {})) {
|
|
319
|
+
samples[cat] = betaSample(
|
|
320
|
+
Math.max(params.alpha, 0.01),
|
|
321
|
+
Math.max(params.beta, 0.01),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return samples;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Internal: Marsaglia-Tsang Gamma Sampling (2000)
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Sample from Gamma(shape, 1) using Marsaglia-Tsang (2000) algorithm.
|
|
333
|
+
* Handles shape < 1 via Johnk's method (shape+1 recursion with U^(1/shape) scaling).
|
|
334
|
+
*
|
|
335
|
+
* @param {number} shape - Shape parameter (must be > 0)
|
|
336
|
+
* @returns {number} Gamma-distributed sample
|
|
337
|
+
*/
|
|
338
|
+
function gammaSample(shape) {
|
|
339
|
+
if (shape < 1) {
|
|
340
|
+
return gammaSample(1 + shape) * Math.pow(Math.random(), 1 / shape);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const d = shape - 1 / 3;
|
|
344
|
+
const c = 1 / Math.sqrt(9 * d);
|
|
345
|
+
|
|
346
|
+
// Rejection sampling loop — terminates quickly for shape >= 1
|
|
347
|
+
// eslint-disable-next-line no-constant-condition
|
|
348
|
+
while (true) {
|
|
349
|
+
let x;
|
|
350
|
+
let v;
|
|
351
|
+
do {
|
|
352
|
+
x = gaussSample();
|
|
353
|
+
v = 1 + c * x;
|
|
354
|
+
} while (v <= 0);
|
|
355
|
+
|
|
356
|
+
v = v * v * v;
|
|
357
|
+
const u = Math.random();
|
|
358
|
+
|
|
359
|
+
if (u < 1 - 0.0331 * (x * x) * (x * x)) {
|
|
360
|
+
return d * v;
|
|
361
|
+
}
|
|
362
|
+
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) {
|
|
363
|
+
return d * v;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Draw a standard normal sample using Box-Muller with rejection sampling.
|
|
370
|
+
* Avoids the log(0) edge case by rejecting s===0.
|
|
371
|
+
*
|
|
372
|
+
* @returns {number} Standard normal sample
|
|
373
|
+
*/
|
|
374
|
+
function gaussSample() {
|
|
375
|
+
let u;
|
|
376
|
+
let v;
|
|
377
|
+
let s;
|
|
378
|
+
do {
|
|
379
|
+
u = Math.random() * 2 - 1;
|
|
380
|
+
v = Math.random() * 2 - 1;
|
|
381
|
+
s = u * u + v * v;
|
|
382
|
+
} while (s >= 1 || s === 0);
|
|
383
|
+
return u * Math.sqrt((-2 * Math.log(s)) / s);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Sample from Beta(alpha, beta) using the gamma ratio method.
|
|
388
|
+
*
|
|
389
|
+
* @param {number} alpha - Alpha shape parameter (> 0)
|
|
390
|
+
* @param {number} beta - Beta shape parameter (> 0)
|
|
391
|
+
* @returns {number} Beta-distributed sample in [0, 1]
|
|
392
|
+
*/
|
|
393
|
+
function betaSample(alpha, beta) {
|
|
394
|
+
const x = gammaSample(alpha);
|
|
395
|
+
const y = gammaSample(beta);
|
|
396
|
+
return x / (x + y);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Exports
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
module.exports = {
|
|
404
|
+
timeDecayWeight,
|
|
405
|
+
loadModel,
|
|
406
|
+
saveModel,
|
|
407
|
+
createInitialModel,
|
|
408
|
+
updateModel,
|
|
409
|
+
getReliability,
|
|
410
|
+
isCalibrated,
|
|
411
|
+
getCalibration,
|
|
412
|
+
samplePosteriors,
|
|
413
|
+
HALF_LIFE_DAYS,
|
|
414
|
+
DECAY_FLOOR,
|
|
415
|
+
MIN_SAMPLES_THRESHOLD,
|
|
416
|
+
DEFAULT_CATEGORIES,
|
|
417
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
searchFeedbackLog,
|
|
6
|
+
searchContextFs,
|
|
7
|
+
searchPreventionRulesSync,
|
|
8
|
+
} = require('./filesystem-search');
|
|
9
|
+
|
|
10
|
+
const VALID_SOURCES = ['all', 'feedback', 'context', 'rules'];
|
|
11
|
+
const SIGNAL_ALIASES = {
|
|
12
|
+
up: 'up',
|
|
13
|
+
positive: 'up',
|
|
14
|
+
down: 'down',
|
|
15
|
+
negative: 'down',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function normalizeSource(source) {
|
|
19
|
+
const normalized = String(source || 'all').trim().toLowerCase() || 'all';
|
|
20
|
+
if (!VALID_SOURCES.includes(normalized)) {
|
|
21
|
+
throw new Error(`source must be one of: ${VALID_SOURCES.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeSignal(signal) {
|
|
27
|
+
if (signal === undefined || signal === null || signal === '') return null;
|
|
28
|
+
const normalized = SIGNAL_ALIASES[String(signal).trim().toLowerCase()];
|
|
29
|
+
if (!normalized) {
|
|
30
|
+
throw new Error('signal must be one of: up, down, positive, negative');
|
|
31
|
+
}
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeRecordSignal(signal) {
|
|
36
|
+
return SIGNAL_ALIASES[String(signal || '').trim().toLowerCase()] || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeLimit(limit) {
|
|
40
|
+
const parsed = Number(limit || 10);
|
|
41
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 10;
|
|
42
|
+
return Math.min(50, Math.floor(parsed));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clampScore(value) {
|
|
46
|
+
const numeric = Number(value || 0);
|
|
47
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
48
|
+
return Number(numeric.toFixed(4));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeArray(values) {
|
|
52
|
+
return Array.isArray(values) ? values : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function excerpt(value, maxLength = 280) {
|
|
56
|
+
const text = String(value || '').trim().replace(/\s+/g, ' ');
|
|
57
|
+
if (!text) return '';
|
|
58
|
+
if (text.length <= maxLength) return text;
|
|
59
|
+
return `${text.slice(0, maxLength - 1)}…`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractFeedbackCorrectiveAction(record) {
|
|
63
|
+
return record.whatToChange
|
|
64
|
+
|| record.what_to_change
|
|
65
|
+
|| record.whatWorked
|
|
66
|
+
|| record.what_worked
|
|
67
|
+
|| null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mapFeedbackResult(record) {
|
|
71
|
+
return {
|
|
72
|
+
id: record.id || null,
|
|
73
|
+
source: 'feedback',
|
|
74
|
+
score: clampScore(record._score),
|
|
75
|
+
signal: normalizeRecordSignal(record.signal),
|
|
76
|
+
tags: safeArray(record.tags),
|
|
77
|
+
timestamp: record.timestamp || null,
|
|
78
|
+
title: record.title || null,
|
|
79
|
+
context: excerpt(record.context || record.message || ''),
|
|
80
|
+
correctiveAction: extractFeedbackCorrectiveAction(record),
|
|
81
|
+
whatWentWrong: record.whatWentWrong || record.what_went_wrong || null,
|
|
82
|
+
whatWorked: record.whatWorked || record.what_worked || null,
|
|
83
|
+
matchedTokens: safeArray(record._matchedTokens),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mapContextResult(record) {
|
|
88
|
+
return {
|
|
89
|
+
id: record.id || null,
|
|
90
|
+
source: 'contextfs',
|
|
91
|
+
score: clampScore(record._score),
|
|
92
|
+
signal: normalizeRecordSignal(record.signal),
|
|
93
|
+
tags: safeArray(record.tags),
|
|
94
|
+
timestamp: record.timestamp || record.createdAt || null,
|
|
95
|
+
title: record.title || null,
|
|
96
|
+
context: excerpt(record.context || record.content || record.title || ''),
|
|
97
|
+
correctiveAction: record.metadata && record.metadata.whatToChange
|
|
98
|
+
? String(record.metadata.whatToChange)
|
|
99
|
+
: null,
|
|
100
|
+
matchedTokens: safeArray(record._matchedTokens),
|
|
101
|
+
namespace: record._namespace || record.namespace || null,
|
|
102
|
+
file: record._source || null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mapRuleResult(record) {
|
|
107
|
+
return {
|
|
108
|
+
id: record.title || null,
|
|
109
|
+
source: 'prevention_rule',
|
|
110
|
+
score: clampScore(record._score || record.score),
|
|
111
|
+
signal: null,
|
|
112
|
+
tags: ['prevention', 'rules'],
|
|
113
|
+
timestamp: null,
|
|
114
|
+
title: record.title || null,
|
|
115
|
+
context: excerpt(record.body || ''),
|
|
116
|
+
correctiveAction: excerpt(record.body || '', 500) || null,
|
|
117
|
+
matchedTokens: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sortResults(results) {
|
|
122
|
+
return [...results].sort((left, right) => {
|
|
123
|
+
if ((right.score || 0) !== (left.score || 0)) {
|
|
124
|
+
return (right.score || 0) - (left.score || 0);
|
|
125
|
+
}
|
|
126
|
+
return String(right.timestamp || '').localeCompare(String(left.timestamp || ''));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getFeedbackResults(query, limit, signal) {
|
|
131
|
+
const results = searchFeedbackLog(query, Math.max(limit * 3, limit));
|
|
132
|
+
const normalizedSignal = normalizeSignal(signal);
|
|
133
|
+
const filtered = normalizedSignal
|
|
134
|
+
? results.filter((record) => normalizeRecordSignal(record.signal) === normalizedSignal)
|
|
135
|
+
: results;
|
|
136
|
+
return filtered.slice(0, limit).map(mapFeedbackResult);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getContextResults(query, limit) {
|
|
140
|
+
return searchContextFs(query, limit).map(mapContextResult);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getRuleResults(query, limit) {
|
|
144
|
+
return searchPreventionRulesSync(query, limit).map(mapRuleResult);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function searchThumbgate({ query, source = 'all', limit = 10, signal = null } = {}) {
|
|
148
|
+
const trimmedQuery = String(query || '').trim();
|
|
149
|
+
if (!trimmedQuery) {
|
|
150
|
+
throw new Error('query is required');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const normalizedSource = normalizeSource(source);
|
|
154
|
+
const normalizedSignal = normalizeSignal(signal);
|
|
155
|
+
const normalizedLimit = normalizeLimit(limit);
|
|
156
|
+
|
|
157
|
+
let results = [];
|
|
158
|
+
if (normalizedSource === 'feedback') {
|
|
159
|
+
results = getFeedbackResults(trimmedQuery, normalizedLimit, normalizedSignal);
|
|
160
|
+
} else if (normalizedSource === 'context') {
|
|
161
|
+
results = getContextResults(trimmedQuery, normalizedLimit);
|
|
162
|
+
} else if (normalizedSource === 'rules') {
|
|
163
|
+
results = getRuleResults(trimmedQuery, normalizedLimit);
|
|
164
|
+
} else {
|
|
165
|
+
results = sortResults([
|
|
166
|
+
...getFeedbackResults(trimmedQuery, normalizedLimit, normalizedSignal),
|
|
167
|
+
...getContextResults(trimmedQuery, normalizedLimit),
|
|
168
|
+
...getRuleResults(trimmedQuery, normalizedLimit),
|
|
169
|
+
]).slice(0, normalizedLimit);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
query: trimmedQuery,
|
|
174
|
+
source: normalizedSource,
|
|
175
|
+
signal: normalizedSignal,
|
|
176
|
+
limit: normalizedLimit,
|
|
177
|
+
engine: 'filesystem-search',
|
|
178
|
+
returned: results.length,
|
|
179
|
+
total: results.length,
|
|
180
|
+
results,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
VALID_SOURCES,
|
|
186
|
+
normalizeSearchSource: normalizeSource,
|
|
187
|
+
normalizeSearchSignal: normalizeSignal,
|
|
188
|
+
searchThumbgate,
|
|
189
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
6
|
+
function getKpiLogPath() { return path.join(resolveFeedbackDir(), 'tool-kpi.jsonl'); }
|
|
7
|
+
function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const raw = fs.readFileSync(fp, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
|
|
8
|
+
function recordToolCall({ toolName, serverName, latencyMs, success, agentId, metadata } = {}) { const lp = getKpiLogPath(); const dir = path.dirname(lp); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const e = { id: `kpi_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, timestamp: new Date().toISOString(), toolName: toolName || 'unknown', serverName: serverName || 'default', latencyMs: typeof latencyMs === 'number' ? latencyMs : 0, success: success !== false, agentId: agentId || 'unknown', metadata: metadata || {} }; fs.appendFileSync(lp, JSON.stringify(e) + '\n'); return e; }
|
|
9
|
+
function percentile(sorted, p) { if (sorted.length === 0) return 0; const idx = Math.ceil((p / 100) * sorted.length) - 1; return sorted[Math.max(0, idx)]; }
|
|
10
|
+
function computeToolKpis({ periodHours = 24 } = {}) { const entries = readJsonl(getKpiLogPath()); const cutoff = Date.now() - periodHours * 60 * 60 * 1000; const recent = entries.filter((e) => new Date(e.timestamp).getTime() > cutoff); const byTool = {}; for (const e of recent) { const k = e.toolName; if (!byTool[k]) byTool[k] = { toolName: k, calls: [], successes: 0, failures: 0 }; byTool[k].calls.push(e.latencyMs); if (e.success) byTool[k].successes++; else byTool[k].failures++; } const tools = Object.values(byTool).map((t) => { const sorted = t.calls.slice().sort((a, b) => a - b); const total = t.successes + t.failures; return { toolName: t.toolName, requestCount: total, successRate: total > 0 ? Math.round((t.successes / total) * 1000) / 10 : 100, p50: Math.round(percentile(sorted, 50)), p90: Math.round(percentile(sorted, 90)), p95: Math.round(percentile(sorted, 95)), successes: t.successes, failures: t.failures }; }).sort((a, b) => b.requestCount - a.requestCount); const byServer = {}; for (const e of recent) { const k = e.serverName; if (!byServer[k]) byServer[k] = { serverName: k, total: 0, successes: 0 }; byServer[k].total++; if (e.success) byServer[k].successes++; } const servers = Object.values(byServer).map((s) => ({ serverName: s.serverName, totalCalls: s.total, successRate: s.total > 0 ? Math.round((s.successes / s.total) * 1000) / 10 : 100 })); return { periodHours, totalCalls: recent.length, tools, servers }; }
|
|
11
|
+
function getAtRiskTools({ successRateThreshold = 90, p95Threshold = 500, periodHours = 24 } = {}) { const { tools } = computeToolKpis({ periodHours }); return tools.filter((t) => t.requestCount >= 3 && (t.successRate < successRateThreshold || t.p95 > p95Threshold)); }
|
|
12
|
+
module.exports = { recordToolCall, computeToolKpis, getAtRiskTools, percentile, getKpiLogPath };
|